Masukkan URL channel YouTube → otomatis ambil semua playlist → semua video → output
getPlayerOptions.playerList. Mendukung @handle,
/channel/UC…, /c/Nama, /user/Nama.
1 URL Channel YouTube
2 Channel Ditemukan
—
—
3 Pilih Playlist
0 playlist
—
4 Output getPlayerOptions.playerList
🎬 0 video
📁 0 playlist
💾 0 KB
Output akan muncul di sini setelah video diambil…
⚠️ Catatan API: YouTube Data API v3 · quota default 10.000 unit/hari.
playlistItems.list = 1 unit/halaman, playlists.list = 1 unit,
search = 100 unit. Pagination otomatis via nextPageToken —
aman untuk playlist dengan 1.000+ video.
. Parser HTML akan masuk ke state
// "script data escaped / double escaped" dan tag penutup
// tidak lagi dikenali -> SyntaxError: Unexpected end of input.
// Semua string berbahaya dipecah dengan konkatenasi.
// ============================================================
const TAG_SCRIPT_OPEN = '<' + 'script type="text/javascript">';
const TAG_SCRIPT_CLOSE = '<' + '/' + 'script>';
const CMT_OPEN = '<' + '!' + '--'; // pembuka komentar HTML
const CMT_CLOSE = '--' + '>'; // penutup komentar HTML
const CDATA_OPEN = '//' + '';
// ============================================================
// STATE
// ============================================================
let currentChannel = null;
let playlists = [];
let playlistsById = {};
let lastJs = '';
let lastHtml = '';
// ============================================================
// DOM
// ============================================================
const $ = (id) => document.getElementById(id);
const statusDot = $('statusDot');
const statusText = $('statusText');
const channelUrl = $('channelUrl');
const btnSearch = $('btnSearch');
const channelCard = $('channelCard');
const chAvatar = $('chAvatar');
const chName = $('chName');
const chMeta = $('chMeta');
const playlistCard = $('playlistCard');
const playlistList = $('playlistList');
const plCount = $('plCount');
const btnSelectAll = $('btnSelectAll');
const btnDeselectAll = $('btnDeselectAll');
const chkUploads = $('chkUploads');
const chkDesc = $('chkDesc');
const chkDescHtml = $('chkDescHtml');
const btnFetchVideos = $('btnFetchVideos');
const btnFetchAll = $('btnFetchAll');
const progress1 = $('progress1');
const progress1Text = $('progress1Text');
const outputCard = $('outputCard');
const output = $('output');
const statVideos = $('statVideos');
const statPlaylists = $('statPlaylists');
const statSize = $('statSize');
const btnCopyJs = $('btnCopyJs');
const btnCopyHtml = $('btnCopyHtml');
const btnDownloadJs = $('btnDownloadJs');
const btnDownloadHtml = $('btnDownloadHtml');
// ============================================================
// UTIL
// ============================================================
function setStatus(state, msg) {
statusDot.className = 'dot' + (state ? ' ' + state : '');
statusText.textContent = msg;
}
function show(el) { el.classList.remove('hidden'); }
function hide(el) { el.classList.add('hidden'); }
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
});
}
function formatNumber(n) {
if (n === undefined || n === null || n === '') return '—';
const num = Number(n);
if (isNaN(num)) return '—';
return num.toLocaleString('id-ID');
}
function downloadFile(filename, text, mime) {
const blob = new Blob([text], { type: mime || 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
}
function jsStr(v) { return JSON.stringify(String(v == null ? '' : v)); }
// Sanitasi isi komentar HTML: cegah "--" menutup komentar lebih awal
function safeComment(s) {
return String(s == null ? '' : s).replace(/--/g, '- -');
}
// ============================================================
// API HELPER
// ============================================================
async function apiGet(endpoint, params) {
const url = new URL(API_BASE + '/' + endpoint);
Object.keys(params).forEach(function (k) {
const v = params[k];
if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, v);
});
url.searchParams.set('key', API_KEY);
const res = await fetch(url.toString());
const data = await res.json().catch(function () { return {}; });
if (!res.ok || data.error) {
throw new Error((data.error && data.error.message) || ('HTTP ' + res.status));
}
return data;
}
// ============================================================
// PARSE URL CHANNEL
// ============================================================
function parseChannelInput(input) {
const raw = String(input || '').trim();
if (!raw) return null;
if (/^UC[\w-]{22}$/.test(raw)) return { type: 'id', value: raw };
if (/^@[\w.\-]+$/.test(raw)) return { type: 'handle', value: raw };
if (/^[\w.\-]+$/.test(raw) && raw.indexOf('.') === -1) {
return { type: 'handle', value: '@' + raw };
}
let url;
try {
url = new URL(raw.indexOf('http') === 0 ? raw : 'https://' + raw);
} catch (e) { return null; }
const parts = url.pathname.split('/').filter(Boolean);
if (!parts.length) return null;
if (parts[0] === 'channel' && parts[1]) return { type: 'id', value: parts[1] };
if (parts[0].charAt(0) === '@') return { type: 'handle', value: parts[0] };
if (parts[0] === 'c' && parts[1]) return { type: 'custom', value: parts[1] };
if (parts[0] === 'user' && parts[1]) return { type: 'username', value: parts[1] };
if (parts.length === 1) return { type: 'handle', value: '@' + parts[0] };
return null;
}
// ============================================================
// RESOLVE CHANNEL
// ============================================================
async function resolveChannel(rawInput) {
const parsed = parseChannelInput(rawInput);
if (!parsed) throw new Error('Format URL channel tidak dikenali.');
let data;
if (parsed.type === 'id') {
data = await apiGet('channels', {
part: 'snippet,contentDetails,statistics',
id: parsed.value
});
} else if (parsed.type === 'handle') {
data = await apiGet('channels', {
part: 'snippet,contentDetails,statistics',
forHandle: parsed.value
});
} else if (parsed.type === 'username') {
data = await apiGet('channels', {
part: 'snippet,contentDetails,statistics',
forUsername: parsed.value
});
} else if (parsed.type === 'custom') {
const search = await apiGet('search', {
part: 'snippet', type: 'channel', q: parsed.value, maxResults: 1
});
if (!search.items || !search.items.length) throw new Error('Channel tidak ditemukan.');
data = await apiGet('channels', {
part: 'snippet,contentDetails,statistics',
id: search.items[0].id.channelId
});
} else {
throw new Error('Tipe URL tidak dikenali.');
}
if (!data.items || !data.items.length) throw new Error('Channel tidak ditemukan.');
const item = data.items[0];
const thumbs = item.snippet.thumbnails || {};
const avatar = (thumbs.medium || thumbs.default || thumbs.high || {}).url || '';
return {
id: item.id,
title: item.snippet.title,
avatar: avatar,
subs: item.statistics ? item.statistics.subscriberCount : null,
videoCount: item.statistics ? item.statistics.videoCount : null,
uploadsPlaylistId: (item.contentDetails && item.contentDetails.relatedPlaylists)
? item.contentDetails.relatedPlaylists.uploads : null
};
}
// ============================================================
// FETCH PLAYLISTS (PAGINATED)
// ============================================================
async function fetchAllPlaylists(channelId) {
const list = [];
let pageToken = '';
do {
const data = await apiGet('playlists', {
part: 'snippet,contentDetails',
channelId: channelId,
maxResults: 50,
pageToken: pageToken || undefined
});
(data.items || []).forEach(function (p) {
const thumbs = p.snippet.thumbnails || {};
list.push({
id: p.id,
title: p.snippet.title,
videoCount: parseInt(p.contentDetails.itemCount, 10) || 0,
thumb: (thumbs.medium || thumbs.default || {}).url || ''
});
});
pageToken = data.nextPageToken || '';
} while (pageToken);
return list;
}
// ============================================================
// FETCH VIDEOS OF A PLAYLIST (PAGINATED)
// ============================================================
async function fetchPlaylistVideos(playlistId, onProgress) {
const out = [];
let pageToken = '';
do {
const data = await apiGet('playlistItems', {
part: 'snippet,contentDetails',
playlistId: playlistId,
maxResults: 50,
pageToken: pageToken || undefined
});
(data.items || []).forEach(function (item) {
const vid = item.snippet && item.snippet.resourceId && item.snippet.resourceId.videoId;
if (!vid) return;
const pos = (item.snippet && typeof item.snippet.position === 'number')
? item.snippet.position : out.length;
out.push({
_id: vid,
position: pos,
file: 'https://www.youtube.com/watch?v=' + vid,
title: (item.snippet && item.snippet.title) || '',
desc: (item.snippet && item.snippet.description) || ''
});
});
if (onProgress) onProgress(out.length);
pageToken = data.nextPageToken || '';
} while (pageToken);
out.sort(function (a, b) { return a.position - b.position; });
return out.map(function (v) {
return { _id: v._id, file: v.file, title: v.title, desc: v.desc };
});
}
// ============================================================
// RENDER CHANNEL
// ============================================================
function renderChannel(ch) {
chAvatar.src = ch.avatar || '';
chAvatar.alt = ch.title;
chName.textContent = ch.title;
chMeta.innerHTML =
'🆔 ' + escapeHtml(ch.id) + ' ' +
'👥 ' + formatNumber(ch.subs) + ' subscriber · ' +
'🎬 ' + formatNumber(ch.videoCount) + ' video';
show(channelCard);
}
// ============================================================
// RENDER PLAYLIST LIST
// ============================================================
function renderPlaylists() {
const saved = {};
playlistList.querySelectorAll('input[type="checkbox"]').forEach(function (cb) {
saved[cb.dataset.plid] = cb.checked;
});
const all = playlists.slice();
if (chkUploads.checked && currentChannel && currentChannel.uploadsPlaylistId) {
if (!all.some(function (p) { return p.id === currentChannel.uploadsPlaylistId; })) {
all.unshift({
id: currentChannel.uploadsPlaylistId,
title: '📺 Uploads — Semua Video Channel',
videoCount: parseInt(currentChannel.videoCount, 10) || 0,
thumb: '',
isUploads: true
});
}
}
playlistsById = {};
all.forEach(function (p) { playlistsById[p.id] = p; });
playlistList.innerHTML = '';
if (!all.length) {
playlistList.innerHTML =
'