import React, { useState, useEffect, useRef } from 'react';
import { Play, Search, Tv, AlertCircle, List, Loader2, ShieldAlert, Wifi, Github, ChevronDown, Clapperboard, Trophy, Music, Newspaper, Baby, Menu, X, RotateCw, Plus, Trash2, Library, FolderOpen, ExternalLink, RefreshCw } from 'lucide-react';
// Helper untuk memuat script HLS.js secara dinamis
const useHlsScript = (src) => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (window.Hls) {
setLoaded(true);
return;
}
const script = document.createElement('script');
script.src = src;
script.async = true;
script.onload = () => setLoaded(true);
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, [src]);
return loaded;
};
const App = () => {
const hlsLoaded = useHlsScript('https://cdn.jsdelivr.net/npm/hls.js@latest');
// -- STATE DATA --
const [playlists, setPlaylists] = useState([]);
const [activePlaylistId, setActivePlaylistId] = useState(null);
// -- STATE INPUT --
const [playlistUrl, setPlaylistUrl] = useState('https://raw.githubusercontent.com/Free-TV/IPTV/master/playlist.m3u8');
const [playlistNameInput, setPlaylistNameInput] = useState('');
const [rawM3U, setRawM3U] = useState('');
const [inputMode, setInputMode] = useState('url');
const [useProxy, setUseProxy] = useState(true);
// -- STATE VIEW / UI --
const [showAddPlaylist, setShowAddPlaylist] = useState(true);
const [channels, setChannels] = useState([]);
const [categories, setCategories] = useState([]);
// -- FILTER STATE --
const [selectedCategory, setSelectedCategory] = useState('All');
const [selectedTheme, setSelectedTheme] = useState('All');
const [filteredChannels, setFilteredChannels] = useState([]);
const [visibleLimit, setVisibleLimit] = useState(50);
const [selectedChannel, setSelectedChannel] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
// -- LAYOUT STATE --
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [isFloatingActive, setIsFloatingActive] = useState(false);
const floatingTimer = useRef(null);
// -- SYSTEM STATE --
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [playerError, setPlayerError] = useState(null); // Error khusus player
const [forceHttps, setForceHttps] = useState(false); // Toggle paksa HTTPS
const videoRef = useRef(null);
const hlsRef = useRef(null);
const listRef = useRef(null);
// Definisi Tema Pintar
const THEMES = [
{ id: 'Sports', label: 'Olahraga', keywords: ['sport', 'soccer', 'football', 'bola', 'racing', 'moto', 'nba', 'ufc', 'wwe', 'tennis', 'golf', 'cricket', 'bein', 'espn', 'fox sports'] },
{ id: 'Movies', label: 'Film & Drama', keywords: ['movie', 'film', 'cinema', 'drama', 'action', 'hbo', 'blockbuster', 'series', 'bioskop'] },
{ id: 'News', label: 'Berita', keywords: ['news', 'berita', 'cnn', 'bbc', 'jazeera', 'tvone', 'kompas', 'metrotv', 'cnbc', 'weather', 'info'] },
{ id: 'Music', label: 'Musik', keywords: ['music', 'musik', 'mtv', 'hits', 'pop', 'dangdut', 'radio', 'concert', 'prambors'] },
{ id: 'Kids', label: 'Anak & Kartun', keywords: ['kids', 'cartoon', 'kartun', 'anime', 'animation', 'disney', 'nick', 'jr', 'toddler', 'baby'] },
{ id: 'Religious', label: 'Religi', keywords: ['islam', 'muslim', 'quran', 'church', 'god', 'tvri', 'mekkah', 'madinah', 'sunnah', 'dakwah'] },
{ id: 'Documentary', label: 'Dokumenter', keywords: ['discovery', 'history', 'geo', 'nature', 'animal', 'wild', 'planet', 'docu'] }
];
// Helper Convert URL
const convertGithubUrl = (url) => {
let newUrl = url.trim();
if (newUrl.endsWith('.git')) newUrl = newUrl.slice(0, -4);
if (newUrl.includes('github.com') && !newUrl.includes('raw.githubusercontent.com')) {
newUrl = newUrl.replace('github.com', 'raw.githubusercontent.com');
newUrl = newUrl.replace('/blob/', '/');
}
return newUrl;
};
// Parsing M3U
const parseM3U = (content) => {
const lines = content.split('\n');
const result = [];
const uniqueGroups = new Set();
let currentChannel = {};
lines.forEach(line => {
line = line.trim();
if (line.startsWith('#EXTINF:')) {
const info = line.substring(8);
const props = info.split(/,(.*)/s);
const logoMatch = line.match(/tvg-logo="([^"]*)"/);
const groupMatch = line.match(/group-title="([^"]*)"/);
const groupName = groupMatch ? groupMatch[1] : 'Uncategorized';
uniqueGroups.add(groupName);
currentChannel = {
name: props[1] || 'Unknown Channel',
logo: logoMatch ? logoMatch[1] : null,
group: groupName,
};
} else if (line.startsWith('http')) {
if (currentChannel.name) {
currentChannel.url = line;
result.push(currentChannel);
currentChannel = {};
}
}
});
const sortedCategories = Array.from(uniqueGroups).sort();
return { result, categories: sortedCategories };
};
// -- CORE LOGIC: Add Playlist --
const addNewPlaylist = (text, type, sourceName = '') => {
if (!text || text.length < 10) throw new Error("Data playlist kosong atau tidak valid.");
const { result, categories } = parseM3U(text);
if (result.length === 0) throw new Error("Tidak ada channel ditemukan.");
const newId = Date.now().toString();
let finalName = playlistNameInput.trim();
if (!finalName) {
finalName = type === 'url' ? (sourceName || 'Web Playlist') : 'Local Playlist';
const count = playlists.length + 1;
finalName = `${finalName} #${count}`;
}
const newPlaylist = {
id: newId,
name: finalName,
channels: result,
categories: categories,
sourceType: type
};
setPlaylists(prev => [...prev, newPlaylist]);
setActivePlaylistId(newId);
setShowAddPlaylist(false);
setPlaylistNameInput('');
if (window.innerWidth < 640) setIsSidebarOpen(true);
};
// -- CORE LOGIC: Switch Playlist --
useEffect(() => {
if (activePlaylistId) {
const active = playlists.find(p => p.id === activePlaylistId);
if (active) {
setChannels(active.channels);
setCategories(active.categories);
setSelectedCategory('All');
setSelectedTheme('All');
setSearchTerm('');
setFilteredChannels(active.channels);
setVisibleLimit(50);
if (active.channels.length > 0) {
if (!selectedChannel) setSelectedChannel(active.channels[0]);
}
}
} else {
setChannels([]);
setFilteredChannels([]);
}
}, [activePlaylistId, playlists]);
const fetchPlaylist = async () => {
setLoading(true);
setError(null);
const targetUrl = convertGithubUrl(playlistUrl);
if (targetUrl !== playlistUrl) setPlaylistUrl(targetUrl);
try {
try {
const response = await fetch(targetUrl);
if (response.ok) {
const text = await response.text();
addNewPlaylist(text, 'url', 'Github/Web Playlist');
setLoading(false);
return;
}
} catch (e) {
console.log("Direct fetch failed, trying proxy...");
}
if (useProxy) {
const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(targetUrl)}`;
const responseProxy = await fetch(proxyUrl);
if (!responseProxy.ok) throw new Error("Gagal mengambil playlist via Proxy.");
const textProxy = await responseProxy.text();
addNewPlaylist(textProxy, 'url', 'Proxy Playlist');
} else {
throw new Error("Koneksi ditolak.");
}
} catch (err) {
setError(`Gagal: ${err.message}`);
} finally {
setLoading(false);
}
};
const loadFromText = () => {
if (!rawM3U) return;
setLoading(true);
try {
addNewPlaylist(rawM3U, 'text');
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const removePlaylist = (e, id) => {
e.stopPropagation();
const newPlaylists = playlists.filter(p => p.id !== id);
setPlaylists(newPlaylists);
if (activePlaylistId === id) {
setActivePlaylistId(newPlaylists.length > 0 ? newPlaylists[0].id : null);
if (newPlaylists.length === 0) setShowAddPlaylist(true);
}
};
useEffect(() => {
let result = channels;
if (selectedCategory !== 'All') {
result = result.filter(ch => ch.group === selectedCategory);
}
if (selectedTheme !== 'All') {
const themeDef = THEMES.find(t => t.id === selectedTheme);
if (themeDef) {
result = result.filter(ch => {
const combinedText = (ch.name + ' ' + ch.group).toLowerCase();
return themeDef.keywords.some(keyword => combinedText.includes(keyword));
});
}
}
if (searchTerm !== '') {
const lowerTerm = searchTerm.toLowerCase();
result = result.filter(ch => ch.name.toLowerCase().includes(lowerTerm));
}
setFilteredChannels(result);
setVisibleLimit(50);
if (listRef.current) listRef.current.scrollTop = 0;
}, [searchTerm, selectedCategory, selectedTheme, channels]);
const handleScroll = (e) => {
const bottom = e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 100;
if (bottom && visibleLimit < filteredChannels.length) {
setVisibleLimit(prev => Math.min(prev + 50, filteredChannels.length));
}
};
const handleChannelSelect = (channel) => {
setSelectedChannel(channel);
setPlayerError(null); // Reset error saat ganti channel
if (window.innerWidth < 640) setIsSidebarOpen(false);
};
const handleFloatingInteraction = () => {
setIsFloatingActive(true);
if (floatingTimer.current) clearTimeout(floatingTimer.current);
floatingTimer.current = setTimeout(() => setIsFloatingActive(false), 3000);
};
const toggleRotation = async () => {
handleFloatingInteraction();
try {
if (!document.fullscreenElement) {
const elem = document.documentElement;
if (elem.requestFullscreen) await elem.requestFullscreen();
else if (elem.webkitRequestFullscreen) await elem.webkitRequestFullscreen();
if (window.screen && window.screen.orientation && window.screen.orientation.lock) {
window.screen.orientation.lock('landscape').catch(e => console.log(e));
}
} else {
if (document.exitFullscreen) await document.exitFullscreen();
else if (document.webkitExitFullscreen) await document.webkitExitFullscreen();
if (window.screen && window.screen.orientation && window.screen.orientation.unlock) {
window.screen.orientation.unlock();
}
}
} catch (err) { console.error(err); }
};
const getThemeIcon = (themeId) => {
switch(themeId) {
case 'Sports': return ;
case 'Movies': return ;
case 'Music': return ;
case 'News': return ;
case 'Kids': return ;
default: return ;
}
};
// -- PLAYER LOGIC UPDATED --
useEffect(() => {
if (!selectedChannel || !hlsLoaded) return;
// Logika URL: Gunakan HTTPS jika dipaksa, jika tidak gunakan asli
let streamUrl = selectedChannel.url;
if (forceHttps && streamUrl.startsWith('http:')) {
streamUrl = streamUrl.replace('http:', 'https:');
}
// Cleanup player lama
if (hlsRef.current) hlsRef.current.destroy();
// Fungsi handle error standard
const handleError = (e) => {
console.error("Media Error:", e);
setPlayerError("Stream gagal diputar. Coba 'Force HTTPS' atau buka di VLC.");
};
if (window.Hls.isSupported()) {
const hls = new window.Hls({
debug: false,
enableWorker: true,
lowLatencyMode: true,
// Konfigurasi agresif untuk recovery
manifestLoadingTimeOut: 15000,
manifestLoadingMaxRetry: 3,
levelLoadingTimeOut: 15000,
levelLoadingMaxRetry: 3,
xhrSetup: function (xhr, url) {
xhr.withCredentials = false; // Hindari masalah credential CORS
},
});
hlsRef.current = hls;
// Load source
hls.loadSource(streamUrl);
hls.attachMedia(videoRef.current);
hls.on(window.Hls.Events.MANIFEST_PARSED, () => {
setPlayerError(null);
videoRef.current.play().catch(e => console.log("Autoplay blocked:", e));
});
hls.on(window.Hls.Events.ERROR, (event, data) => {
if (data.fatal) {
switch (data.type) {
case window.Hls.ErrorTypes.NETWORK_ERROR:
console.log("Network error, trying to recover...");
hls.startLoad();
break;
case window.Hls.ErrorTypes.MEDIA_ERROR:
console.log("Media error, trying to recover...");
hls.recoverMediaError();
break;
default:
hls.destroy();
setPlayerError("Format tidak didukung atau diblokir browser.");
break;
}
}
});
} else if (videoRef.current.canPlayType('application/vnd.apple.mpegurl')) {
// Native Safari HLS
videoRef.current.src = streamUrl;
videoRef.current.addEventListener('loadedmetadata', () => {
setPlayerError(null);
videoRef.current.play();
});
videoRef.current.addEventListener('error', handleError);
} else {
// Fallback untuk MP4 dll
videoRef.current.src = streamUrl;
videoRef.current.play().catch(handleError);
}
return () => { if (hlsRef.current) hlsRef.current.destroy(); };
}, [selectedChannel, hlsLoaded, forceHttps]);
return (
{/* Floating Rotation Button */}
{/* Header */}
{isSidebarOpen && (
setIsSidebarOpen(false)} />
)}
{/* Sidebar */}
{/* Main Content */}
{!hlsLoaded ? (
) : !selectedChannel ? (
IPTV Player
{playlists.length === 0 ? "Silakan tambah playlist baru di sidebar." : "Pilih channel dari library playlist Anda."}
) : (
{/* Protocol Warning & Controls */}
{selectedChannel.url.startsWith('http:') && !forceHttps && (
HTTP Stream terdeteksi. Jika gagal, coba tombol 'Force HTTPS' di bawah.
)}
{/* Video Player */}
{/* Error Overlay / Recovery Options */}
{playerError && (
Gagal Memutar Stream
{playerError}
Buka Langsung (VLC)
Tips: Browser modern memblokir konten HTTP. Gunakan opsi 'Buka Langsung' untuk hasil terbaik.
)}
{/* Channel Name Overlay */}
)}
);
};
export default App;