Download index.html from Robshoods/iptv: direct link, hf CLI and curl.
- Browser
- Download file 19.1 kB
-
https://huggingface.co/spaces/Robshoods/iptv/resolve/main/index.html
- Command line
-
hf download hf://spaces/Robshoods/iptv/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Robshoods/iptv/resolve/main/index.html
19.1 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>IPTV Player</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .video-container { | |
| aspect-ratio: 16/9; | |
| } | |
| .channel-list { | |
| max-height: calc(100vh - 200px); | |
| overflow-y: auto; | |
| } | |
| .category-item { | |
| transition: all 0.2s ease; | |
| } | |
| .category-item:hover { | |
| transform: translateX(5px); | |
| } | |
| .channel-item { | |
| transition: all 0.2s ease; | |
| } | |
| .channel-item:hover { | |
| background-color: rgba(255, 255, 255, 0.1); | |
| } | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #1a202c; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #4a5568; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #718096; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-gray-100"> | |
| <div class="container mx-auto px-4 py-6"> | |
| <header class="flex justify-between items-center mb-8"> | |
| <h1 class="text-3xl font-bold text-blue-400"> | |
| <i class="fas fa-tv mr-2"></i> IPTV Player | |
| </h1> | |
| <div class="flex items-center space-x-4"> | |
| <div class="relative"> | |
| <input type="text" id="searchInput" placeholder="Search channels..." | |
| class="bg-gray-800 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 w-64"> | |
| <i class="fas fa-search absolute right-3 top-3 text-gray-400"></i> | |
| </div> | |
| <button id="loadPlaylistBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center"> | |
| <i class="fas fa-cloud-download-alt mr-2"></i> Load Playlist | |
| </button> | |
| <input type="file" id="fileInput" accept=".m3u,.m3u8" class="hidden"> | |
| </div> | |
| </header> | |
| <div class="grid grid-cols-1 lg:grid-cols-4 gap-6"> | |
| <!-- Sidebar with categories and channels --> | |
| <div class="lg:col-span-1 bg-gray-800 rounded-xl p-4"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-list-alt mr-2 text-blue-400"></i> Categories | |
| </h2> | |
| <div id="categoryList" class="space-y-2 mb-6"> | |
| <!-- Categories will be loaded here --> | |
| <div class="text-gray-400 italic">No categories loaded</div> | |
| </div> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-tv mr-2 text-blue-400"></i> Channels | |
| </h2> | |
| <div id="channelList" class="channel-list"> | |
| <!-- Channels will be loaded here --> | |
| <div class="text-gray-400 italic">Select a category to view channels</div> | |
| </div> | |
| </div> | |
| <!-- Main content area with video player --> | |
| <div class="lg:col-span-3 space-y-6"> | |
| <div class="bg-gray-800 rounded-xl overflow-hidden"> | |
| <div class="video-container bg-black"> | |
| <video id="videoPlayer" controls class="w-full h-full" poster="https://via.placeholder.com/1280x720/1a202c/4a5568?text=Select+a+channel"> | |
| Your browser does not support the video tag. | |
| </video> | |
| </div> | |
| </div> | |
| <div class="bg-gray-800 rounded-xl p-4"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-info-circle mr-2 text-blue-400"></i> Now Playing | |
| </h2> | |
| <div id="nowPlayingInfo" class="space-y-2"> | |
| <div class="text-2xl font-bold" id="currentChannelName">No channel selected</div> | |
| <div class="text-gray-400" id="currentChannelGroup">-</div> | |
| <div class="flex items-center text-gray-400" id="currentChannelUrl"> | |
| <i class="fas fa-link mr-2"></i> - | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-gray-800 rounded-xl p-4"> | |
| <h2 class="text-xl font-semibold mb-4 flex items-center"> | |
| <i class="fas fa-history mr-2 text-blue-400"></i> Recently Played | |
| </h2> | |
| <div id="recentChannels" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3"> | |
| <div class="text-gray-400 italic">No recent channels</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const videoPlayer = document.getElementById('videoPlayer'); | |
| const loadPlaylistBtn = document.getElementById('loadPlaylistBtn'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const searchInput = document.getElementById('searchInput'); | |
| const categoryList = document.getElementById('categoryList'); | |
| const channelList = document.getElementById('channelList'); | |
| const currentChannelName = document.getElementById('currentChannelName'); | |
| const currentChannelGroup = document.getElementById('currentChannelGroup'); | |
| const currentChannelUrl = document.getElementById('currentChannelUrl'); | |
| const recentChannels = document.getElementById('recentChannels'); | |
| let playlistData = []; | |
| let categories = []; | |
| let recentItems = JSON.parse(localStorage.getItem('recentChannels')) || []; | |
| // Load recent channels | |
| updateRecentChannels(); | |
| // Event listeners | |
| loadPlaylistBtn.addEventListener('click', function() { | |
| fileInput.click(); | |
| }); | |
| fileInput.addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| parseM3UFile(file); | |
| } | |
| }); | |
| searchInput.addEventListener('input', function() { | |
| filterChannels(this.value); | |
| }); | |
| // Parse M3U file | |
| function parseM3UFile(file) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const content = e.target.result; | |
| const lines = content.split('\n'); | |
| playlistData = []; | |
| categories = []; | |
| for (let i = 0; i < lines.length; i++) { | |
| if (lines[i].startsWith('#EXTINF:')) { | |
| const channelInfo = parseExtInf(lines[i]); | |
| const channelUrl = lines[i+1]; | |
| if (channelInfo && channelUrl) { | |
| playlistData.push({ | |
| ...channelInfo, | |
| url: channelUrl.trim() | |
| }); | |
| if (channelInfo.group && !categories.includes(channelInfo.group)) { | |
| categories.push(channelInfo.group); | |
| } | |
| } | |
| } | |
| } | |
| displayCategories(); | |
| }; | |
| reader.readAsText(file); | |
| } | |
| // Parse EXTINF line | |
| function parseExtInf(line) { | |
| const regex = /#EXTINF:-?[0-9]*\s*(?:tvg-id="([^"]*)")?\s*(?:tvg-name="([^"]*)")?\s*(?:tvg-logo="([^"]*)")?\s*(?:group-title="([^"]*)")?\s*(?:,[^"]*)?(.*)/; | |
| const match = line.match(regex); | |
| if (match) { | |
| return { | |
| tvgId: match[1] || '', | |
| tvgName: match[2] || '', | |
| tvgLogo: match[3] || '', | |
| group: match[4] || 'Uncategorized', | |
| name: match[5] || 'Unnamed Channel' | |
| }; | |
| } | |
| return null; | |
| } | |
| // Display categories | |
| function displayCategories() { | |
| categoryList.innerHTML = ''; | |
| if (categories.length === 0) { | |
| categoryList.innerHTML = '<div class="text-gray-400 italic">No categories found</div>'; | |
| return; | |
| } | |
| // Sort categories alphabetically | |
| categories.sort(); | |
| // Add "All Channels" option | |
| const allItem = document.createElement('div'); | |
| allItem.className = 'category-item cursor-pointer p-2 rounded-lg bg-gray-700 text-white font-medium flex items-center justify-between'; | |
| allItem.innerHTML = ` | |
| <span>All Channels</span> | |
| <span class="bg-blue-600 text-xs px-2 py-1 rounded-full">${playlistData.length}</span> | |
| `; | |
| allItem.addEventListener('click', function() { | |
| displayChannels(playlistData); | |
| }); | |
| categoryList.appendChild(allItem); | |
| // Add each category | |
| categories.forEach(category => { | |
| const count = playlistData.filter(ch => ch.group === category).length; | |
| const item = document.createElement('div'); | |
| item.className = 'category-item cursor-pointer p-2 rounded-lg hover:bg-gray-700 text-gray-300 hover:text-white flex items-center justify-between'; | |
| item.innerHTML = ` | |
| <span>${category}</span> | |
| <span class="bg-gray-600 text-xs px-2 py-1 rounded-full">${count}</span> | |
| `; | |
| item.addEventListener('click', function() { | |
| const categoryChannels = playlistData.filter(ch => ch.group === category); | |
| displayChannels(categoryChannels); | |
| }); | |
| categoryList.appendChild(item); | |
| }); | |
| } | |
| // Display channels | |
| function displayChannels(channels) { | |
| channelList.innerHTML = ''; | |
| if (channels.length === 0) { | |
| channelList.innerHTML = '<div class="text-gray-400 italic">No channels found</div>'; | |
| return; | |
| } | |
| channels.forEach(channel => { | |
| const item = document.createElement('div'); | |
| item.className = 'channel-item cursor-pointer p-3 rounded-lg hover:bg-gray-700 flex items-center space-x-3'; | |
| item.innerHTML = ` | |
| <div class="w-10 h-10 bg-gray-600 rounded-full overflow-hidden flex-shrink-0"> | |
| ${channel.tvgLogo ? | |
| `<img src="${channel.tvgLogo}" alt="${channel.name}" class="w-full h-full object-cover">` : | |
| `<div class="w-full h-full flex items-center justify-center text-xs">${channel.name.substring(0, 2)}</div>`} | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="font-medium truncate">${channel.name}</div> | |
| <div class="text-xs text-gray-400 truncate">${channel.group}</div> | |
| </div> | |
| <i class="fas fa-play text-blue-400"></i> | |
| `; | |
| item.addEventListener('click', function() { | |
| playChannel(channel); | |
| }); | |
| channelList.appendChild(item); | |
| }); | |
| } | |
| // Play channel | |
| function playChannel(channel) { | |
| currentChannelName.textContent = channel.name; | |
| currentChannelGroup.textContent = channel.group; | |
| currentChannelUrl.innerHTML = `<i class="fas fa-link mr-2"></i> ${channel.url}`; | |
| // Try to play the video | |
| videoPlayer.src = channel.url; | |
| videoPlayer.load(); | |
| videoPlayer.play().catch(e => { | |
| console.error('Error playing video:', e); | |
| alert('Error playing this channel. It may be unavailable or your browser may not support the stream format.'); | |
| }); | |
| // Add to recent channels | |
| addToRecent(channel); | |
| } | |
| // Add to recent channels | |
| function addToRecent(channel) { | |
| // Remove if already exists | |
| recentItems = recentItems.filter(item => item.url !== channel.url); | |
| // Add to beginning | |
| recentItems.unshift({ | |
| name: channel.name, | |
| group: channel.group, | |
| logo: channel.tvgLogo, | |
| url: channel.url | |
| }); | |
| // Keep only last 8 items | |
| if (recentItems.length > 8) { | |
| recentItems = recentItems.slice(0, 8); | |
| } | |
| // Save to localStorage | |
| localStorage.setItem('recentChannels', JSON.stringify(recentItems)); | |
| // Update UI | |
| updateRecentChannels(); | |
| } | |
| // Update recent channels display | |
| function updateRecentChannels() { | |
| recentChannels.innerHTML = ''; | |
| if (recentItems.length === 0) { | |
| recentChannels.innerHTML = '<div class="text-gray-400 italic">No recent channels</div>'; | |
| return; | |
| } | |
| recentItems.forEach(channel => { | |
| const item = document.createElement('div'); | |
| item.className = 'cursor-pointer group'; | |
| item.innerHTML = ` | |
| <div class="aspect-video bg-gray-700 rounded-lg overflow-hidden relative group-hover:opacity-90 transition-opacity"> | |
| ${channel.logo ? | |
| `<img src="${channel.logo}" alt="${channel.name}" class="w-full h-full object-cover">` : | |
| `<div class="w-full h-full flex items-center justify-center text-gray-400"> | |
| <i class="fas fa-tv text-3xl"></i> | |
| </div>`} | |
| <div class="absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <i class="fas fa-play text-white text-2xl"></i> | |
| </div> | |
| </div> | |
| <div class="mt-2"> | |
| <div class="font-medium text-sm truncate">${channel.name}</div> | |
| <div class="text-xs text-gray-400 truncate">${channel.group}</div> | |
| </div> | |
| `; | |
| item.addEventListener('click', function() { | |
| playChannel(channel); | |
| }); | |
| recentChannels.appendChild(item); | |
| }); | |
| } | |
| // Filter channels by search term | |
| function filterChannels(term) { | |
| if (!term) { | |
| if (categoryList.querySelector('.bg-gray-700')) { | |
| // If a category is selected, show its channels | |
| categoryList.querySelector('.bg-gray-700').click(); | |
| } else { | |
| // Otherwise show all channels | |
| displayChannels(playlistData); | |
| } | |
| return; | |
| } | |
| const filtered = playlistData.filter(channel => | |
| channel.name.toLowerCase().includes(term.toLowerCase()) || | |
| channel.group.toLowerCase().includes(term.toLowerCase()) | |
| ); | |
| displayChannels(filtered); | |
| } | |
| // Example: Load a sample playlist URL | |
| function loadSamplePlaylist() { | |
| const sampleUrl = 'http://cdn.sasdscg.com/p/883696456724/389011997309/m3u'; | |
| fetch(sampleUrl) | |
| .then(response => { | |
| if (!response.ok) throw new Error('Network response was not ok'); | |
| return response.text(); | |
| }) | |
| .then(text => { | |
| // Simulate file parsing | |
| const blob = new Blob([text], {type: 'text/plain'}); | |
| const file = new File([blob], 'sample.m3u'); | |
| parseM3UFile(file); | |
| }) | |
| .catch(error => { | |
| console.error('Error loading sample playlist:', error); | |
| alert('Error loading sample playlist. Please try loading your own M3U file.'); | |
| }); | |
| } | |
| // Uncomment to enable loading a sample playlist on startup | |
| // loadSamplePlaylist(); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Robshoods/iptv" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |