CoolFace
Apppublic

kinostream4/asil-tv

sourceHugging Faceupdated 11d agoView on Hugging Face
0likes
index.html629 linesDownload Raw Back to root
1<!DOCTYPE html>2<html lang="uz">3<head>4<meta charset="UTF-8">5<meta name="viewport" content="width=device-width, initial-scale=1.0">6<title>Universal IPTV Editor - Group Logo Support</title>7<style>8    :root {9        --bg-color: #121212;10        --card-bg: #1e1e1e;11        --text-main: #e0e0e0;12        --text-muted: #a0a0a0;13        --accent: #bb86fc;14        --accent-hover: #9965f4;15        --success: #03dac6;16        --danger: #cf6679;17        --border: #333;18    }19 20    * { margin: 0; padding: 0; box-sizing: border-box; outline: none; }21 22    body {23        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;24        background-color: var(--bg-color);25        color: var(--text-main);26        min-height: 100vh;27        display: flex;28        justify-content: center;29        padding: 20px;30    }31 32    .container {33        width: 100%;34        max-width: 1100px;35        background: var(--card-bg);36        border-radius: 16px;37        box-shadow: 0 10px 30px rgba(0,0,0,0.5);38        overflow: hidden;39        border: 1px solid var(--border);40    }41 42    .header {43        background: linear-gradient(135deg, #3700b3, #6200ee);44        padding: 20px;45        text-align: center;46        color: white;47    }48 49    .content { padding: 20px; }50 51    .upload-area {52        border: 2px dashed var(--accent);53        border-radius: 12px;54        padding: 30px;55        text-align: center;56        cursor: pointer;57        background: rgba(187, 134, 252, 0.05);58        margin-bottom: 20px;59        transition: 0.3s;60    }61    .upload-area:hover { background: rgba(187, 134, 252, 0.1); border-color: var(--accent-hover); }62 63    .top-controls {64        display: flex;65        gap: 10px;66        margin-bottom: 20px;67        flex-wrap: wrap;68    }69 70    .btn {71        border: none;72        padding: 10px 20px;73        border-radius: 8px;74        cursor: pointer;75        font-weight: bold;76        transition: 0.2s;77        display: inline-flex;78        align-items: center;79        gap: 5px;80    }81    .btn-primary { background: var(--accent); color: #000; }82    .btn-success { background: var(--success); color: #000; }83    .btn-danger { background: var(--danger); color: #fff; }84    .btn-sort { background: #ff9800; color: #000; }85 86    .preview-box {87        max-height: 65vh;88        overflow-y: auto;89        border: 1px solid var(--border);90        border-radius: 8px;91        background: #181818;92    }93 94    /* Scrollbar */95    .preview-box::-webkit-scrollbar { width: 8px; }96    .preview-box::-webkit-scrollbar-track { background: #121212; }97    .preview-box::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }98 99    .group-card { 100        border-bottom: 1px solid var(--border); 101        background: #222;102    }103    104    .group-header {105        padding: 15px;106        background: #2a2a2a;107        display: flex;108        align-items: center;109        gap: 15px;110        position: sticky;111        top: 0;112        z-index: 10;113        border-left: 4px solid var(--accent);114    }115 116    /* Group Logo Style */117    .group-logo-img {118        width: 50px;119        height: 50px;120        object-fit: contain;121        background: #000;122        border-radius: 8px;123        border: 1px solid #555;124    }125 126    .group-title { 127        flex: 1; 128        font-weight: bold; 129        color: var(--accent); 130        display: flex; 131        justify-content: space-between; 132        align-items: center;133        font-size: 1.1rem;134    }135 136    .action-btn {137        width: 32px; height: 32px;138        border-radius: 50%; border: none;139        cursor: pointer;140        display: flex; align-items: center; justify-content: center;141        color: white; margin-left: 5px;142        font-size: 14px;143        transition: transform 0.2s;144    }145    .action-btn:hover { transform: scale(1.1); }146 147    .btn-move-up { background: #ff9800; }148    .btn-move-down { background: #2196f3; }149    .btn-edit { background: #9c27b0; }150    .btn-toggle { background: #607d8b; }151    .btn-group-logo { background: #e91e63; } /* Pink for Group Logo */152 153    .channel-row {154        display: flex;155        align-items: center;156        padding: 8px 15px;157        border-top: 1px solid #333;158        transition: background 0.2s;159    }160    .channel-row:hover { background: #333; }161 162    /* Channel Logo Image Style */163    .ch-logo-img {164        width: 40px;165        height: 40px;166        object-fit: contain;167        background: #000;168        border-radius: 4px;169        margin-right: 10px;170        border: 1px solid #444;171    }172 173    .channel-info { flex: 1; overflow: hidden; }174    .ch-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 2px;}175    .ch-url { font-size: 0.7rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }176 177    .ch-actions { display: flex; gap: 5px; margin-left: 5px; }178    179    .ch-move-btn, .ch-logo-btn {180        width: 28px; height: 28px;181        background: #333; color: #ccc;182        border-radius: 4px; border: none;183        cursor: pointer;184        display: flex; align-items: center; justify-content: center;185        font-size: 12px;186    }187    .ch-move-btn:hover { background: var(--accent); color: #000; }188    .ch-logo-btn:hover { background: #00bcd4; color: #fff; } 189 190    .toast {191        position: fixed; bottom: 20px; left: 50%;192        transform: translateX(-50%);193        background: var(--success); color: #000;194        padding: 12px 24px; border-radius: 30px;195        font-weight: bold; z-index: 1000;196        box-shadow: 0 4px 12px rgba(0,0,0,0.3);197    }198</style>199</head>200<body>201 202<div class="container">203    <div class="header">204        <h1>Universal IPTV Editor PRO</h1>205        <p>Guruh va Kanal Logolari + Aqlli Saralash</p>206    </div>207 208    <div class="content">209        <div class="upload-area" id="uploadArea">210            <span style="font-size: 40px;">๐Ÿ“‚</span>211            <h3>M3U faylni tashlang</h3>212            <input type="file" id="fileInput" multiple accept=".m3u,.m3u8" hidden>213        </div>214 215        <div class="top-controls">216            <button class="btn btn-primary" id="selectAllBtn">โœ… Hammasi</button>217            <button class="btn btn-danger" id="deselectAllBtn">โŒ Tozalash</button>218            <button class="btn btn-sort" id="autoSortBtn">๐Ÿ”ข Avto-Saralash</button>219            <button class="btn btn-success" id="saveBtn"> Saqlash (M3U)</button>220        </div>221 222        <div id="preview" class="preview-box"></div>223    </div>224</div>225 226<script>227class IPTVEditor {228    constructor() {229        this.channels = [];230        this.selectedIds = new Set();231        this.groupRenameMap = {};232        // Guruh logolarini saqlash uchun obyekt233        this.groupLogos = {}; 234        this.init();235    }236 237    init() {238        const uploadArea = document.getElementById('uploadArea');239        const fileInput = document.getElementById('fileInput');240 241        uploadArea.onclick = () => fileInput.click();242        fileInput.addEventListener('change', (e) => this.loadFiles(e.target.files));243 244        document.getElementById('selectAllBtn').onclick = () => {245            this.channels.forEach(ch => this.selectedIds.add(ch.id));246            this.render();247        };248 249        document.getElementById('deselectAllBtn').onclick = () => {250            this.selectedIds.clear();251            this.render();252        };253 254        document.getElementById('saveBtn').onclick = () => this.exportM3U();255        document.getElementById('autoSortBtn').onclick = () => this.smartSortAll();256    }257 258    loadFiles(files) {259        if (!files.length) return;260        this.channels = [];261        this.groupLogos = {}; // Yangi fayl yuklanganda logolarni tozalash262        const reader = new FileReader();263        reader.onload = (e) => {264            this.parseM3U(e.target.result);265            this.removeDuplicates();266            this.render();267            this.showMessage('โœ… Fayl yuklandi!');268        };269        reader.readAsText(files[0], 'utf-8');270    }271 272    parseM3U(content) {273        const lines = content.split(/\r?\n/);274        let extinf = null;275 276        for (let line of lines) {277            line = line.trim();278            if (line.startsWith('#EXTINF:')) {279                extinf = line;280            } else if (line && !line.startsWith('#') && extinf) {281                const name = extinf.split(',').pop().trim();282                let group = 'Boshqa';283                let logo = '';284 285                const grp = extinf.match(/group-title="([^"]+)"/);286                if (grp) group = grp[1];287                288                const lg = extinf.match(/tvg-logo="([^"]+)"/);289                if (lg) logo = lg[1];290 291                group = group.replace(/[๐Ÿ“‚]/g, '').trim();292 293                this.channels.push({294                    id: 'ch_' + Date.now() + '_' + Math.random(),295                    name,296                    group,297                    logo,298                    url: line299                });300                extinf = null;301            }302        }303    }304 305    removeDuplicates() {306        const unique = new Map();307        this.channels.forEach(ch => {308            const key = ch.name.toLowerCase() + '|' + ch.url.toLowerCase();309            if (!unique.has(key)) unique.set(key, ch);310        });311        this.channels = Array.from(unique.values());312    }313 314    // --- SMART SORT ---315    extractNumber(str) {316        const match = str.match(/\d+/);317        return match ? parseInt(match[0], 10) : null;318    }319 320    smartSortAll() {321        const groupsMap = {};322        const groupOrder = [];323 324        this.channels.forEach(ch => {325            const g = this.groupRenameMap[ch.group] || ch.group;326            if (!groupsMap[g]) {327                groupsMap[g] = [];328                groupOrder.push(g);329            }330            groupsMap[g].push(ch);331        });332 333        let newChannels = [];334        335        groupOrder.forEach(gName => {336            const groupChannels = groupsMap[gName];337            groupChannels.sort((a, b) => {338                const numA = this.extractNumber(a.name);339                const numB = this.extractNumber(b.name);340                if (numA !== null && numB !== null) return numA - numB;341                if (numA !== null) return -1;342                if (numB !== null) return 1;343                return a.name.localeCompare(b.name);344            });345            newChannels = newChannels.concat(groupChannels);346        });347 348        this.channels = newChannels;349        this.render();350        this.showMessage(' Kanallar 1, 2, 3 tartibida saralandi!');351    }352 353    // --- MOVE LOGIC ---354    moveGroupUp(groupName) { this.reorderGroups(groupName, -1); }355    moveGroupDown(groupName) { this.reorderGroups(groupName, 1); }356 357    reorderGroups(targetGroup, direction) {358        let currentOrder = [];359        let seen = new Set();360        this.channels.forEach(ch => {361            let g = this.groupRenameMap[ch.group] || ch.group;362            if (!seen.has(g)) { currentOrder.push(g); seen.add(g); }363        });364 365        const index = currentOrder.indexOf(targetGroup);366        if (index < 0) return;367        const newIndex = index + direction;368        if (newIndex < 0 || newIndex >= currentOrder.length) return;369 370        [currentOrder[index], currentOrder[newIndex]] = [currentOrder[newIndex], currentOrder[index]];371 372        let newChannels = [];373        currentOrder.forEach(gName => {374            this.channels.forEach(ch => {375                let chGroup = this.groupRenameMap[ch.group] || ch.group;376                if (chGroup === gName) newChannels.push(ch);377            });378        });379 380        this.channels = newChannels;381        this.render();382    }383 384    moveChannelUp(channelId) { this.moveChannel(channelId, -1); }385    moveChannelDown(channelId) { this.moveChannel(channelId, 1); }386 387    moveChannel(targetId, direction) {388        const targetIndex = this.channels.findIndex(ch => ch.id === targetId);389        if (targetIndex < 0) return;390        const newIndex = targetIndex + direction;391        if (newIndex < 0 || newIndex >= this.channels.length) return;392 393        const targetGroup = this.groupRenameMap[this.channels[targetIndex].group] || this.channels[targetIndex].group;394        const neighborGroup = this.groupRenameMap[this.channels[newIndex].group] || this.channels[newIndex].group;395 396        if (targetGroup !== neighborGroup) return;397 398        [this.channels[targetIndex], this.channels[newIndex]] = [this.channels[newIndex], this.channels[targetIndex]];399        this.render();400    }401 402    // --- LOGO EDIT FUNCTIONS ---403 404    // Kanal logosini o'zgartirish405    editChannelLogo(channelId) {406        const channel = this.channels.find(ch => ch.id === channelId);407        if (!channel) return;408 409        const newLogo = prompt("Kanal uchun yangi Logo URL:", channel.logo);410        if (newLogo !== null) {411            channel.logo = newLogo.trim();412            this.render();413            this.showMessage('๏ธ Kanal logosi yangilandi!');414        }415    }416 417    // Guruh logosini o'zgartirish418    editGroupLogo(groupName) {419        // Hozirgi nomni tekshirish (agar rename bo'lgan bo'lsa)420        const realGroupName = Object.keys(this.groupRenameMap).find(key => this.groupRenameMap[key] === groupName) || groupName;421        422        const currentLogo = this.groupLogos[realGroupName] || '';423        const newLogo = prompt(`"${groupName}" guruhi uchun Logo URL:`, currentLogo);424        425        if (newLogo !== null) {426            this.groupLogos[realGroupName] = newLogo.trim();427            428            // Ixtiyoriy: Barcha kanallarga ham shu logoni qo'yishmi?429            const applyToAll = confirm("Bu logoni ushbu guruhdagi BARCHA kanallarga ham qo'yilsinmi?");430            431            if (applyToAll) {432                this.channels.forEach(ch => {433                    let chGroup = this.groupRenameMap[ch.group] || ch.group;434                    if (chGroup === groupName) {435                        ch.logo = newLogo.trim();436                    }437                });438            }439 440            this.render();441            this.showMessage(' Guruh logosi yangilandi!');442        }443    }444 445    // --- RENDER ---446    render() {447        const preview = document.getElementById('preview');448        preview.innerHTML = '';449 450        const groupsMap = {};451        const groupOrder = [];452 453        this.channels.forEach(ch => {454            const g = this.groupRenameMap[ch.group] || ch.group;455            if (!groupsMap[g]) {456                groupsMap[g] = [];457                groupOrder.push(g);458            }459            groupsMap[g].push(ch);460        });461 462        if (groupOrder.length === 0) {463            preview.innerHTML = '<div style="padding:20px; text-align:center; color:#666;">Hali hech nima yuklanmagan</div>';464            return;465        }466 467        groupOrder.forEach(groupName => {468            const channels = groupsMap[groupName];469            470            const groupDiv = document.createElement('div');471            groupDiv.className = 'group-card';472 473            const header = document.createElement('div');474            header.className = 'group-header';475            476            const cb = document.createElement('input');477            cb.type = 'checkbox';478            cb.checked = channels.every(ch => this.selectedIds.has(ch.id));479            cb.onchange = () => {480                if(cb.checked) channels.forEach(c => this.selectedIds.add(c.id));481                else channels.forEach(c => this.selectedIds.delete(c.id));482                this.render();483            };484 485            // Guruh Logosi486            // Asl guruh nomini topish (rename map orqali)487            const originalGroupName = Object.keys(this.groupRenameMap).find(key => this.groupRenameMap[key] === groupName) || groupName;488            const groupLogoUrl = this.groupLogos[originalGroupName] || 'https://via.placeholder.com/50x50/333/fff?text=Grp';489 490            const groupLogoImg = document.createElement('img');491            groupLogoImg.className = 'group-logo-img';492            groupLogoImg.src = groupLogoUrl;493            groupLogoImg.onerror = function() { this.src = 'https://via.placeholder.com/50x50/333/fff?text=Err'; };494 495            const titleWrap = document.createElement('div');496            titleWrap.className = 'group-title';497            titleWrap.innerHTML = `<span>${groupName} <span style="background:#333; padding:2px 8px; border-radius:10px; font-size:0.8em; color:#fff;">${channels.length}</span></span>`;498 499            const btnsDiv = document.createElement('div');500            btnsDiv.style.display = 'flex';501            btnsDiv.style.gap = '5px';502 503            const createBtn = (text, cls, onClick, title='') => {504                const btn = document.createElement('button');505                btn.className = `action-btn ${cls}`;506                btn.innerText = text;507                btn.title = title;508                btn.onclick = onClick;509                return btn;510            };511 512            btnsDiv.append(513                createBtn('โฌ†๏ธ', 'btn-move-up', () => this.moveGroupUp(groupName), "Guruhni tepaga"),514                createBtn('โฌ‡๏ธ', 'btn-move-down', () => this.moveGroupDown(groupName), "Guruhni pastga"),515                createBtn('๐Ÿ–ผ๏ธ', 'btn-group-logo', () => this.editGroupLogo(groupName), "Guruh logosini o'zgartirish"),516                createBtn('๐Ÿ‘๏ธ', 'btn-toggle', () => {517                    listDiv.style.display = listDiv.style.display === 'none' ? 'block' : 'none';518                }, "Yashirish/Ko'rsatish"),519                createBtn('โœ๏ธ', 'btn-edit', () => {520                    const newName = prompt("Guruh nomini o'zgartirish:", groupName);521                    if(newName && newName.trim()) {522                        channels.forEach(c => c.group = newName);523                        this.groupRenameMap[groupName] = newName;524                        this.render();525                    }526                }, "Nomini o'zgartirish")527            );528 529            titleWrap.append(btnsDiv);530            header.append(cb, groupLogoImg, titleWrap);531            groupDiv.append(header);532 533            const listDiv = document.createElement('div');534            listDiv.className = 'channel-list';535            listDiv.style.display = 'block'; 536 537            channels.forEach(ch => {538                const row = document.createElement('div');539                row.className = 'channel-row';540 541                const chCb = document.createElement('input');542                chCb.type = 'checkbox';543                chCb.checked = this.selectedIds.has(ch.id);544                chCb.onchange = () => {545                    if(chCb.checked) this.selectedIds.add(ch.id);546                    else this.selectedIds.delete(ch.id);547                };548 549                // Kanal Logosi550                const logoImg = document.createElement('img');551                logoImg.className = 'ch-logo-img';552                logoImg.src = ch.logo ? ch.logo : 'https://via.placeholder.com/40x40/333/fff?text=No+Logo';553                logoImg.onerror = function() { this.src = 'https://via.placeholder.com/40x40/333/fff?text=Err'; };554 555                const info = document.createElement('div');556                info.className = 'channel-info';557                info.innerHTML = `<div class="ch-name">๐Ÿ“บ ${ch.name}</div><div class="ch-url">${ch.url}</div>`;558 559                const chActions = document.createElement('div');560                chActions.className = 'ch-actions';561                562                const chUp = document.createElement('button');563                chUp.className = 'ch-move-btn';564                chUp.innerText = '';565                chUp.title = "Tepaga";566                chUp.onclick = () => this.moveChannelUp(ch.id);567 568                const chDown = document.createElement('button');569                chDown.className = 'ch-move-btn';570                chDown.innerText = 'โฌ‡';571                chDown.title = "Pastga";572                chDown.onclick = () => this.moveChannelDown(ch.id);573 574                const chLogoBtn = document.createElement('button');575                chLogoBtn.className = 'ch-logo-btn';576                chLogoBtn.innerText = '๐Ÿ–ผ๏ธ';577                chLogoBtn.title = "Kanal logosini o'zgartirish";578                chLogoBtn.onclick = () => this.editChannelLogo(ch.id);579 580                chActions.append(chUp, chDown, chLogoBtn);581                582                row.append(chCb, logoImg, info, chActions);583                listDiv.append(row);584            });585 586            groupDiv.append(listDiv);587            preview.append(groupDiv);588        });589    }590 591    exportM3U() {592        const selected = this.channels.filter(ch => this.selectedIds.has(ch.id));593        if (!selected.length) {594            this.showMessage('โŒ Hech nima tanlanmagan!');595            return;596        }597 598        let m3u = '#EXTM3U\n';599        selected.forEach(ch => {600            let line = `#EXTINF:-1 group-title="${ch.group}"`;601            if (ch.logo) line += ` tvg-logo="${ch.logo}"`;602            line += `,${ch.name}\n${ch.url}\n`;603            m3u += line;604        });605 606        const blob = new Blob([m3u], { type: 'application/x-mpegURL' });607        const url = URL.createObjectURL(blob);608        const a = document.createElement('a');609        a.href = url;610        a.download = 'final_playlist_with_group_logos.m3u';611        a.click();612        URL.revokeObjectURL(url);613        this.showMessage(`โœ… Saqlandi: ${selected.length} ta kanal`);614    }615 616    showMessage(msg) {617        const toast = document.createElement('div');618        toast.className = 'toast';619        toast.innerText = msg;620        document.body.appendChild(toast);621        setTimeout(() => toast.remove(), 3000);622    }623}624 625new IPTVEditor();626</script>627 628</body>629</html>