qrtzcode
HomeDocsAPILeaderboardChangelog
Contribute
Docs

qrtzcode

Kumpulan gist & snippet pribadi — anime, AI tools, scraper, dan randomness.

Navigate

HomeDocsAPI Reference

Links

© 2026 qrtz. All snippets welcome.

ESC

Navigate

Links

Library

Roseveil Novel Scraper (SvelteKit JSON Parser)

​• Scraper ringan untuk mengambil data dari web Roseveil.org tanpa library tambahan (seperti Cheerio atau Puppeteer). • Memanfaatkan fitur fetch bawaan JavaScript dan Regex untuk mengekstrak data JSON SvelteKit langsung dari elemen <script>. • Berisi 3 fungsi utama yang siap di-import ke project berbasis Node.js: getHomeData (untuk Popular/Latest), getNovelDetail (berdasarkan slug), dan getChapterContent. • Feel free untuk memodifikasi atau menghapus komentar di bagian "Contoh Penggunaan" paling bawah sesuai kebutuhan.

Note

Ini cuman proyek gabut,jd kalo kurang rapih atau udah gak work fix sndiri aja 😊

Contributed by

@7771 contribution

Creator

777

Language

javascript

Views

7

Copies

1

Base

—

Updated

13 Agu 2026

#scraper#javascript#crawler#api#novel#roseveil#fetch#sveltekit

Code

71
roseveil-scraper.js
// roseveil-scraper.js
// Scraper untuk Roseveil.org
// Menggunakan fetch (Node.js 18+ atau browser)

const BASE_URL = 'https://roseveil.org';

async function fetcher(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status} - ${response.statusText}`);
  }
  const data = await response.text();
  return { data };
}

function extractScriptData(html, urlPattern) {
  const regex = new RegExp(
    `<script[^>]*data-sveltekit-fetched[^>]*data-url="([^"]*${urlPattern}[^"]*)"[^>]*>([\\s\\S]*?)<\\/script>`,
    'i'
  );
  const match = html.match(regex);
  if (!match) return null;

  try {
    const json = JSON.parse(match[2]);
    const parsedBody = JSON.parse(json.body);
    return parsedBody;
  } catch (_) {
    return null;
  }
}

export async function getHomeData() {
  try {
    const url = `${BASE_URL}/novels`;
    const { data: html } = await fetcher(url);

    const popularData = extractScriptData(html, '/api/search\\?sort=popular');
    const latestData = extractScriptData(html, '/api/search\\?sort=latest');

    const popular = (popularData?.data || [])
      .filter(item => item.type === 'NOVEL')
      .map(item => ({
        id: item.id,
        title: item.title,
        slug: item.slug,
        cover: item.poster_image_url,
        rating: item.rating_average,
        views: item.view_count,
        followers: item.followers_count,
        author: item.author_name,
        status: item.series_status,
        type: item.novel_subtype,
        genres: (item.genres || []).map(g => g.name || g),
        synopsis: item.synopsis,
      }));

    const latest = (latestData?.data || [])
      .filter(item => item.type === 'NOVEL')
      .map(item => ({
        id: item.id,
        title: item.title,
        slug: item.slug,
        cover: item.poster_image_url,
        rating: item.rating_average,
        views: item.view_count,
        followers: item.followers_count,
        author: item.author_name,
        status: item.series_status,
        type: item.novel_subtype,
        genres: (item.genres || []).map(g => g.name || g),
        synopsis: item.synopsis,
      }));

    return { popular, latest };
  } catch (error) {
    throw new Error(`Gagal mengambil data home: ${error.message}`);
  }
}

export async function getNovelDetail(slug) {
  if (!slug) throw new Error('Parameter slug wajib diisi');

  try {
    const url = `${BASE_URL}/novels/${slug}`;
    const { data: html } = await fetcher(url);

    const parsed = extractScriptData(html, '/api/series/novels/');
    if (!parsed) throw new Error('Data novel tidak ditemukan');

    const {
      id,
      title,
      slug: seriesSlug,
      synopsis,
      poster_image_url,
      rating_average,
      view_count,
      followers_count,
      author_name,
      publisher_name,
      series_status,
      novel_subtype,
      genres,
      units,
      uploaders,
    } = parsed;

    return {
      id,
      title,
      slug: seriesSlug,
      synopsis: synopsis || 'Tidak ada sinopsis',
      cover: poster_image_url,
      rating: rating_average || '0.0',
      views: view_count || 0,
      followers: followers_count || 0,
      author: author_name || 'Unknown',
      publisher: publisher_name || 'Unknown',
      status: series_status || 'Unknown',
      type: novel_subtype || 'NOVEL',
      genres: (genres || []).map(g => g.name || g),
      chapters: (units || []).map(ch => ({
        number: ch.chapter_number || ch.number,
        title: ch.title || `Chapter ${ch.chapter_number || ch.number}`,
        slug: ch.slug,
        coin_price: ch.coin_price || 0,
        is_premium: ch.is_premium || false,
        is_locked: ch.is_locked || false,
        created_at: ch.created_at || null,
      })),
      uploaders: (uploaders || []).map(u => ({
        username: u.username,
        avatar: u.avatar_url,
        role: u.role,
      })),
    };
  } catch (error) {
    throw new Error(`Gagal mengambil detail novel: ${error.message}`);
  }
}

export async function getChapterContent(slug, chapter) {
  if (!slug || !chapter) throw new Error('Parameter slug dan chapter wajib diisi');

  try {
    const url = `${BASE_URL}/novels/${slug}/chapter/${chapter}`;
    const { data: html } = await fetcher(url);

    const parsed = extractScriptData(html, '/api/series/novels/.*?/chapter/');
    if (!parsed) throw new Error('Data chapter tidak ditemukan');

    const { series, chapter: chapterData, units, previous_chapter, next_chapter } = parsed;

    return {
      series: {
        id: series.id,
        title: series.title,
        slug: series.slug,
        cover: series.poster_image_url,
        rating: series.rating_average,
      },
      chapter: {
        number: chapterData.chapter_number,
        title: chapterData.title,
        slug: chapterData.slug,
        content: chapterData.content || '',
        word_count: chapterData.word_count || 0,
        is_premium: chapterData.is_premium || false,
        is_locked: chapterData.is_locked || false,
        coin_price: chapterData.coin_price || 0,
        created_at: chapterData.created_at,
      },
      chapter_list: (units || []).map(ch => ({
        number: ch.chapter_number,
        title: ch.title,
        slug: ch.slug,
        is_premium: ch.is_premium || false,
        is_locked: ch.is_locked || false,
      })),
      navigation: {
        previous: previous_chapter
          ? {
              number: previous_chapter.chapter_number,
              title: previous_chapter.title,
              slug: previous_chapter.slug,
            }
          : null,
        next: next_chapter
          ? {
              number: next_chapter.chapter_number,
              title: next_chapter.title,
              slug: next_chapter.slug,
            }
          : null,
      },
    };
  } catch (error) {
    throw new Error(`Gagal mengambil chapter: ${error.message}`);
  }
}

// Contoh penggunaan (jika di-run langsung)
// (async () => {
//   try {
//     const home = await getHomeData();
//     console.log('Home:', home);
//     const detail = await getNovelDetail('judul-novel');
//     console.log('Detail:', detail);
//     const chapter = await getChapterContent('judul-novel', '1');
//     console.log('Chapter:', chapter);
//   } catch (e) {
//     console.error(e);
//   }
// })();

Rating

5.0(1)

Gimana snippet ini menurutmu?

</> Embed

Embed ke website / blog

<iframe src="https://qrtzcode.vercel.app/api/embed/library/roseveil-novel-scraper-sveltekit-json-parser" style="width:100%;height:400px;border:none;border-radius:12px;"></iframe>