melolo.js

javascriptCreated 5 Des 2025, 09:28247 views
Watch short dramas and video reels on melolo
#utility#video#downloader
javascript
/***
  @ Base: https://play.google.com/store/apps/details?id=com.worldance.drama/
  @ Author: Shannz
  @ Note: watch short dramas and video reels
***/

import axios from 'axios';
import fs from 'fs';

const generateRandomId = (length = 19) => {
    let result = '';
    result += Math.floor(Math.random() * 9) + 1; 
    for (let i = 1; i < length; i++) {
        result += Math.floor(Math.random() * 10);
    }
    return result;
};

const generateOpenUdid = () => {
    return 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, () => {
        return (Math.random() * 16 | 0).toString(16);
    });
};

const generateUUID = () => {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        const r = Math.random() * 16 | 0;
        const v = c === 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
};

const CONFIG = {
    BASE_URL: "https://api.tmtreader.com",
    HEADERS: {
        "Host": "api.tmtreader.com",
        "Accept": "application/json; charset=utf-8,application/x-protobuf",
        "X-Xs-From-Web": "false",
        "Age-Range": "8",
        "Sdk-Version": "2",
        "Passport-Sdk-Version": "50357",
        "X-Vc-Bdturing-Sdk-Version": "2.2.1.i18n",
        "User-Agent": "ScRaPe/9.9 (KaliLinux; Nusantara Os; My/Shannz)" //"com.worldance.drama/49819 (Linux; U; Android 9; in; SM-N976N; Build/QP1A.190711.020;tt-ok/3.12.13.17)",
    },

    COMMON_PARAMS: {
        "iid": generateRandomId(19),
        "device_id": generateRandomId(19),
        "ac": "wifi",
        "channel": "gp",
        "aid": "645713",
        "app_name": "Melolo",
        "version_code": "49819",
        "version_name": "4.9.8",
        "device_platform": "android",
        "os": "android",
        "ssmix": "a",
        "device_type": "ScRaPe",
        "device_brand": "Shannz",
        "language": "in",
        "os_api": "28",
        "os_version": "15",
        "openudid": generateOpenUdid(),
        "manifest_version_code": "49819",
        "resolution": "900*1600",
        "dpi": "320",
        "update_version_code": "49819",
        "current_region": "ID",
        "carrier_region": "ID",
        "app_language": "id",
        "sys_language": "in",
        "app_region": "ID",
        "sys_region": "ID",
        "mcc_mnc": "46002",
        "carrier_region_v2": "460",
        "user_language": "id",
        "time_zone": "Asia/Jakarta",
        "ui_language": "in",
        "cdid": generateUUID(),
    }
};

const generateRticket = () => {
    return String(Math.floor(Date.now() * 1000) + Math.floor(Math.random() * 1000));
};

const request = async (method, endpoint, params = {}, data = null, customHeaders = {}) => {
    try {
        const url = `${CONFIG.BASE_URL}${endpoint}`;

        const finalParams = {
            ...CONFIG.COMMON_PARAMS,
            ...params,
            "_rticket": generateRticket()
        };

        const config = {
            method,
            url,
            headers: { ...CONFIG.HEADERS, ...customHeaders },
            params: finalParams,
            data
        };

        const response = await axios(config);
        return response.data;
    } catch (error) {
        const errorMsg = error.response ? JSON.stringify(error.response.data) : error.message;
        throw new Error(`Melolo API Error: ${errorMsg}`);
    }
};

export const melolo = {
    search: async (query, offset = 0, limit = 10) => {
        const endpoint = '/i18n_novel/search/page/v1/';
        const params = {
            "search_source_id": "clks###",
            "IsFetchDebug": "false",
            "offset": offset,
            "cancel_search_category_enhance": "false",
            "query": query,
            "limit": limit,
            "search_id": ""
        };

        const json = await request('GET', endpoint, params);
        const searchData = json?.data?.search_data || [];
        const results = [];

        if (Array.isArray(searchData)) {
            searchData.forEach(section => {
                if (section.books && Array.isArray(section.books)) {
                    section.books.forEach(book => {
                        results.push({
                            title: book.book_name,
                            book_id: book.book_id,
                            cover: book.thumb_url,
                            author: book.author,
                            sinopsis: book.abstract,
                            status: book.show_creation_status,
                            tags: book.stat_infos || [],
                            total_chapters: book.serial_count || book.last_chapter_index
                        });
                    });
                }
            });
        }
        
        return results;
    },

    detail: async (bookId) => {
        if (!bookId) throw new Error("Book ID required");

        const endpoint = '/novel/player/video_detail/v1/';

        const headers = {
            "X-Ss-Stub": "238B6268DE1F0B757306031C76B5397E",
            "Content-Type": "application/json; charset=utf-8"
        };

        const payload = {
            "biz_param": {
                "detail_page_version": 0,
                "from_video_id": "",
                "need_all_video_definition": false,
                "need_mp4_align": false,
                "source": 4,
                "use_os_player": false,
                "video_id_type": 1
            },
            "series_id": bookId
        };

        const json = await request('POST', endpoint, {}, payload, headers);
        const data = json?.data?.video_data || {}; 

        let tags = [];
        try {
            if (data.category_schema) {
                const parsed = JSON.parse(data.category_schema);
                tags = parsed.map(cat => cat.name);
            }
        } catch (e) {
            console.warn("Gagal parse category_schema");
        }

        const videoList = data.video_list || [];
        const episodes = videoList.map(v => ({
            video_id: v.vid,
            episode: v.vid_index,
            title: v.title,
            duration: v.duration,
            likes: v.digged_count,
            cover: v.cover
        }));

        return {
            book_id: data.series_id_str || bookId,
            title: data.series_title,
            intro: data.series_intro,
            cover: data.series_cover,
            total_episodes: data.episode_cnt,
            tags: tags,
            status: data.series_status === 1 ? "Ongoing" : "Completed",
            episodes: episodes
        };
    },

    stream: async (videoId) => {
        if (!videoId) throw new Error("Video ID required");

        const endpoint = '/novel/player/video_model/v1/';

        const headers = {
            "X-Ss-Stub": "B7FB786F2CAA8B9EFB7C67A524B73AFB",
            "Content-Type": "application/json; charset=utf-8"
        };

        const payload = {
            "biz_param": {
                "detail_page_version": 0,
                "device_level": 3,
                "from_video_id": "",
                "need_all_video_definition": true,
                "need_mp4_align": false,
                "source": 4,
                "use_os_player": false,
                "video_id_type": 0,
                "video_platform": 3
            },
            "video_id": videoId
        };

        const json = await request('POST', endpoint, {}, payload, headers);
        const raw = json?.data || {};

        let result = {
            status: true,
            url: raw.main_url,
            backup_url: raw.backup_url,
            expire_at: raw.expire_time,
            width: raw.video_width,
            height: raw.video_height,
            metadata: {},
            downloads: []
        };

        try {
            if (raw.video_model) {
                const model = JSON.parse(raw.video_model);
                const thumbs = model.big_thumbs || [];
                result.metadata = {
                    id: model.video_id,
                    duration: model.video_duration,
                    thumbnail: thumbs.length > 0 ? thumbs[0].img_url : null
                };

                if (model.video_list) {
                    Object.values(model.video_list).forEach(item => {
                        let videoUrl = item.main_url;

                        if (videoUrl && !videoUrl.startsWith('http')) {
                            try {
                                videoUrl = Buffer.from(videoUrl, 'base64').toString('utf-8');
                            } catch (err) {
                                console.warn("Gagal decode URL untuk kualitas:", item.definition);
                            }
                        }

                        result.downloads.push({
                            quality: item.definition,
                            size: item.size,
                            fps: item.fps,
                            url: videoUrl
                        });
                    });

                    result.downloads.sort((a, b) => b.size - a.size);
                }
            }
        } catch (error) {
            console.error("Error parsing video_model:", error.message);
            result.status = false;
            result.error = "Failed to parse detailed video model";
        }

        return result;
    }
};