Загрузить файлы в «templates»
This commit is contained in:
245
templates/Playlist.html
Normal file
245
templates/Playlist.html
Normal file
@@ -0,0 +1,245 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }}</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: #0f0f0f; color: #eee; font-family: Arial, sans-serif; }
|
||||
.container { max-width: 960px; margin: 30px auto; padding: 0 16px 40px; }
|
||||
.back-btn {
|
||||
display: inline-block; margin-bottom: 14px;
|
||||
background: #2a2a2a; color: #aaa; border: 1px solid #444;
|
||||
border-radius: 6px; padding: 7px 14px; font-size: 13px;
|
||||
cursor: pointer; text-decoration: none;
|
||||
}
|
||||
.back-btn:hover { color: #fff; border-color: #888; }
|
||||
.player-wrap { background: #000; border-radius: 10px; overflow: hidden; }
|
||||
video { width: 100%; display: block; max-height: 480px; }
|
||||
|
||||
/* Controls */
|
||||
.controls {
|
||||
display: flex; flex-wrap: wrap; gap: 12px;
|
||||
align-items: flex-end; padding: 14px 0;
|
||||
}
|
||||
.controls label { font-size: 12px; color: #aaa; display: block; margin-bottom: 5px; }
|
||||
select {
|
||||
background: #1e1e1e; color: #fff; border: 1px solid #444;
|
||||
border-radius: 6px; padding: 6px 10px; font-size: 13px; cursor: pointer;
|
||||
}
|
||||
.btn {
|
||||
background: #ff4444; color: #fff; border: none;
|
||||
border-radius: 6px; padding: 8px 20px; font-size: 13px;
|
||||
cursor: pointer; font-weight: bold;
|
||||
}
|
||||
.btn:hover { background: #cc0000; }
|
||||
.btn:disabled { background: #555; cursor: not-allowed; }
|
||||
.dl-btn {
|
||||
background: #1a7a1a; color: #fff; border: none;
|
||||
border-radius: 6px; padding: 8px 20px; font-size: 13px;
|
||||
cursor: pointer; font-weight: bold;
|
||||
}
|
||||
.dl-btn:hover { background: #145214; }
|
||||
|
||||
/* Now playing */
|
||||
.now-playing {
|
||||
background: #1a1a1a; border-radius: 10px;
|
||||
padding: 14px 18px; margin-top: 4px;
|
||||
font-size: 14px; color: #ccc;
|
||||
}
|
||||
.now-playing span { color: #fff; font-weight: bold; }
|
||||
|
||||
/* Playlist sidebar */
|
||||
.layout { display: flex; gap: 16px; margin-top: 20px; }
|
||||
.sidebar {
|
||||
width: 320px; flex-shrink: 0;
|
||||
background: #1a1a1a; border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sidebar-header {
|
||||
padding: 14px 16px; border-bottom: 1px solid #2a2a2a;
|
||||
font-size: 14px; font-weight: bold;
|
||||
}
|
||||
.sidebar-header span { color: #aaa; font-weight: normal; font-size: 12px; }
|
||||
.playlist-items {
|
||||
overflow-y: auto; max-height: 480px;
|
||||
}
|
||||
.playlist-item {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 14px; cursor: pointer;
|
||||
border-bottom: 1px solid #222;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.playlist-item:hover { background: #252525; }
|
||||
.playlist-item.active { background: #2a1a1a; border-left: 3px solid #ff4444; }
|
||||
.item-index { color: #666; font-size: 12px; min-width: 22px; }
|
||||
.item-info { flex: 1; overflow: hidden; }
|
||||
.item-title {
|
||||
font-size: 13px; white-space: nowrap;
|
||||
overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.item-meta { font-size: 11px; color: #666; margin-top: 2px; }
|
||||
|
||||
/* Description */
|
||||
.card { background: #1a1a1a; border-radius: 10px; padding: 20px; flex: 1; }
|
||||
.card h2 { font-size: 16px; margin-bottom: 8px; }
|
||||
.card .meta { font-size: 13px; color: #aaa; }
|
||||
|
||||
/* Download status */
|
||||
.dl-status { font-size: 12px; color: #aaa; margin-top: 6px; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a href="/" class="back-btn">← New video</a>
|
||||
|
||||
<!-- Player -->
|
||||
<div class="player-wrap">
|
||||
<video id="player" controls autoplay preload="auto">
|
||||
Your browser doesn't support HTML5 video.
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<div>
|
||||
<label>🎬 Quality</label>
|
||||
<select id="quality-select">
|
||||
<option value="best">Best available</option>
|
||||
<option value="bestvideo[height<=1080]">1080p</option>
|
||||
<option value="bestvideo[height<=720]">720p</option>
|
||||
<option value="bestvideo[height<=480]">480p</option>
|
||||
<option value="bestvideo[height<=360]">360p</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn" id="prev-btn" onclick="prevVideo()">⏮ Prev</button>
|
||||
<button class="btn" id="next-btn" onclick="nextVideo()">Next ⏭</button>
|
||||
<div>
|
||||
<label>⬇ Playlist</label>
|
||||
<button class="dl-btn" onclick="downloadPlaylist()">⬇ Download all (ZIP)</button>
|
||||
<div class="dl-status" id="dl-status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Now playing -->
|
||||
<div class="now-playing" id="now-playing">
|
||||
Loading first video…
|
||||
</div>
|
||||
|
||||
<!-- Layout: info card + playlist sidebar -->
|
||||
<div class="layout">
|
||||
<div class="card">
|
||||
<h2>{{ title }}</h2>
|
||||
<div class="meta">
|
||||
📺 {{ uploader }} · {{ video_count }} videos
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
Playlist <span>{{ video_count }} videos</span>
|
||||
</div>
|
||||
<div class="playlist-items" id="playlist-items">
|
||||
{% for entry in entries %}
|
||||
<div class="playlist-item" id="item-{{ loop.index0 }}"
|
||||
onclick="playAt({{ loop.index0 }})">
|
||||
<span class="item-index">{{ loop.index }}</span>
|
||||
<div class="item-info">
|
||||
<div class="item-title">{{ entry.title }}</div>
|
||||
<div class="item-meta">{{ entry.uploader }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
const player = document.getElementById('player');
|
||||
const playlistUrl = {{ playlist_url | tojson }};
|
||||
const totalVideos = {{ video_count }};
|
||||
let currentIndex = 0;
|
||||
|
||||
function getFormatId() {
|
||||
return document.getElementById('quality-select').value;
|
||||
}
|
||||
|
||||
function playAt(index) {
|
||||
if (index < 0 || index >= totalVideos) return;
|
||||
currentIndex = index;
|
||||
|
||||
// Update active item in sidebar
|
||||
document.querySelectorAll('.playlist-item').forEach(el =>
|
||||
el.classList.remove('active')
|
||||
);
|
||||
const activeEl = document.getElementById(`item-${index}`);
|
||||
if (activeEl) {
|
||||
activeEl.classList.add('active');
|
||||
activeEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Update now playing label
|
||||
const titleEl = activeEl?.querySelector('.item-title');
|
||||
document.getElementById('now-playing').innerHTML =
|
||||
`Now playing <span>#${index + 1} — ${titleEl?.textContent || ''}</span>`;
|
||||
|
||||
// Update video source
|
||||
const streamUrl =
|
||||
`/playlist/stream?url=${encodeURIComponent(playlistUrl)}&index=${index}&format_id=${getFormatId()}`;
|
||||
|
||||
player.src = streamUrl;
|
||||
player.load();
|
||||
player.play();
|
||||
|
||||
// Update prev/next buttons
|
||||
document.getElementById('prev-btn').disabled = index === 0;
|
||||
document.getElementById('next-btn').disabled = index === totalVideos - 1;
|
||||
}
|
||||
|
||||
function prevVideo() { playAt(currentIndex - 1); }
|
||||
function nextVideo() { playAt(currentIndex + 1); }
|
||||
|
||||
// Auto-advance to next video when current ends
|
||||
player.addEventListener('ended', () => {
|
||||
if (currentIndex < totalVideos - 1) {
|
||||
nextVideo();
|
||||
}
|
||||
});
|
||||
|
||||
// Download entire playlist as ZIP
|
||||
async function downloadPlaylist() {
|
||||
const status = document.getElementById('dl-status');
|
||||
status.style.display = 'block';
|
||||
status.textContent = '⏳ Preparing ZIP…';
|
||||
|
||||
// Poll progress while server processes
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const r = await fetch(
|
||||
`/playlist/progress?url=${encodeURIComponent(playlistUrl)}`
|
||||
);
|
||||
const data = await r.json();
|
||||
if (data.total) {
|
||||
status.textContent =
|
||||
`⬇ Processing video ${data.current}/${data.total}: ${data.video_title || ''}`;
|
||||
}
|
||||
} catch {}
|
||||
}, 1500);
|
||||
|
||||
window.location.href =
|
||||
`/playlist/download?url=${encodeURIComponent(playlistUrl)}&format_id=${getFormatId()}`;
|
||||
|
||||
setTimeout(() => {
|
||||
clearInterval(pollInterval);
|
||||
status.textContent = '✅ Download started — check your browser downloads.';
|
||||
setTimeout(() => { status.style.display = 'none'; }, 5000);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Start playing first video on load
|
||||
playAt(0);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user