Загрузить файлы в «templates»

This commit is contained in:
2026-03-22 13:42:40 +00:00
parent 3c2b60a963
commit c0311f4dcc
3 changed files with 633 additions and 0 deletions

178
templates/Home.html Normal file
View File

@@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>YT Stream Player</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #0f0f0f; color: #eee; font-family: Arial, sans-serif;
min-height: 100vh; display: flex; align-items: center; justify-content: center;
}
.card {
background: #1a1a1a; border-radius: 14px; padding: 40px;
width: 100%; max-width: 540px; box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.logo { font-size: 36px; text-align: center; margin-bottom: 8px; }
h1 { text-align: center; font-size: 22px; margin-bottom: 6px; }
.subtitle { text-align: center; color: #888; font-size: 13px; margin-bottom: 28px; }
label { font-size: 13px; color: #aaa; display: block; margin-bottom: 6px; }
input[type="text"] {
width: 100%; background: #111; border: 1px solid #333; border-radius: 8px;
color: #fff; font-size: 14px; padding: 12px 14px; outline: none;
transition: border-color 0.2s;
}
input[type="text"]:focus { border-color: #ff4444; }
input[type="text"]::placeholder { color: #555; }
.btn {
width: 100%; margin-top: 16px; background: #ff4444; color: #fff;
border: none; border-radius: 8px; padding: 13px; font-size: 15px;
font-weight: bold; cursor: pointer; transition: background 0.2s;
}
.btn:hover { background: #cc0000; }
.btn:disabled { background: #555; cursor: not-allowed; }
.error {
margin-top: 14px; background: #3a1a1a; border: 1px solid #ff4444;
border-radius: 8px; padding: 10px 14px; font-size: 13px;
color: #ff8888; display: none;
}
.loading { display: none; text-align: center; margin-top: 16px; color: #aaa; font-size: 13px; }
.spinner {
display: inline-block; width: 16px; height: 16px; border: 2px solid #555;
border-top-color: #ff4444; border-radius: 50%;
animation: spin 0.7s linear infinite; vertical-align: middle; margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.examples { margin-top: 22px; border-top: 1px solid #2a2a2a; padding-top: 16px; }
.examples p { font-size: 12px; color: #666; margin-bottom: 10px; }
.example-links { display: flex; flex-direction: column; gap: 6px; }
.example-link {
background: #222; border: 1px solid #333; border-radius: 6px;
padding: 8px 12px; font-size: 12px; color: #888; cursor: pointer;
transition: border-color 0.2s, color 0.2s; text-align: left;
}
.example-link:hover { border-color: #ff4444; color: #eee; }
.supported { margin-top: 16px; text-align: center; font-size: 11px; color: #555; }
</style>
</head>
<body>
<div class="card">
<div class="logo">▶️</div>
<h1>YT Stream Player</h1>
<p class="subtitle">Paste any YouTube link to stream or download</p>
<label for="url-input">YouTube URL</label>
<input type="text" id="url-input"
placeholder="https://www.youtube.com/watch?v=..." autofocus>
<button class="btn" id="watch-btn" onclick="goWatch()">▶ Watch Now</button>
<div class="error" id="error-box"></div>
<div class="loading" id="loading">
<span class="spinner"></span> Fetching video info, please wait...
</div>
<div class="examples">
<p>Supported URL formats:</p>
<div class="example-links">
<button class="example-link"
onclick="fillUrl('https://www.youtube.com/watch?v=jNQXAC9IVRw')">
📺 youtube.com/watch?v=...
</button>
<button class="example-link"
onclick="fillUrl('https://youtu.be/jNQXAC9IVRw')">
🔗 youtu.be/...
</button>
<button class="example-link"
onclick="fillUrl('https://www.youtube.com/shorts/dQw4w9WgXcQ')">
📱 youtube.com/shorts/...
</button>
</div>
</div>
<p class="supported">Supports streaming and direct download</p>
</div>
<script>
const input = document.getElementById('url-input');
const btn = document.getElementById('watch-btn');
const errBox = document.getElementById('error-box');
const loader = document.getElementById('loading');
input.addEventListener('keydown', e => { if (e.key === 'Enter') goWatch(); });
input.addEventListener('input', () => { errBox.style.display = 'none'; });
function fillUrl(url) { input.value = url; input.focus(); errBox.style.display = 'none'; }
function isValidYoutubeUrl(url) {
return /^https?:\/\/(www\.)?(youtube\.com\/(watch\?v=|shorts\/)|youtu\.be\/)/.test(url);
}
async function goWatch() {
const url = input.value.trim();
if (!url) { showError('Please paste a YouTube URL first.'); return; }
if (!isValidYoutubeUrl(url)) { showError("That doesn't look like a valid YouTube URL."); return; }
btn.disabled = true;
loader.style.display = 'block';
errBox.style.display = 'none';
try {
const res = await fetch('/validate', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ url })
});
const data = await res.json();
if (data.ok) {
window.location.href = '/watch?url=' + encodeURIComponent(url);
} else {
showError(data.error || 'Could not load video.');
}
} catch (e) {
showError('Server error. Is Flask running?');
} finally {
btn.disabled = false;
loader.style.display = 'none';
}
}
function showError(msg) {
errBox.textContent = '⚠ ' + msg;
errBox.style.display = 'block';
}
async function goWatch() {
const url = input.value.trim();
if (!url) { showError('Please paste a YouTube URL.'); return; }
btn.disabled = true;
loader.style.display = 'block';
errBox.style.display = 'none';
try {
const res = await fetch(`/info?url=${encodeURIComponent(url)}`);
const data = await res.json();
if (!res.ok) {
showError(data.detail || 'Could not load URL.');
return;
}
// ✅ Route to playlist or video player based on type
if (data.type === 'playlist') {
window.location.href =
`/playlist/watch?url=${encodeURIComponent(url)}`;
} else {
window.location.href =
`/watch?url=${encodeURIComponent(url)}`;
}
} catch (e) {
showError('Server error.');
} finally {
btn.disabled = false;
loader.style.display = 'none';
}
}
</script>
</body>
</html>

210
templates/Player.html Normal file
View File

@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ meta.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; }
.topbar {
display: flex; justify-content: space-between;
align-items: center; margin-bottom: 14px;
}
.back-btn {
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: 540px; }
/* 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;
}
select:hover { border-color: #888; }
.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; }
.dl-btn {
background: #1a7a1a; color: #fff; border: none;
border-radius: 6px; padding: 8px 20px; font-size: 13px;
cursor: pointer; font-weight: bold; text-decoration: none;
display: inline-block;
}
.dl-btn:hover { background: #145214; }
.dl-btn.loading { background: #555; cursor: not-allowed; pointer-events: none; }
.dl-status {
font-size: 12px; color: #aaa; margin-top: 6px;
display: none; min-width: 200px;
}
.progress-bar-wrap {
background: #333; border-radius: 4px; height: 6px;
margin-top: 4px; overflow: hidden; display: none;
}
.progress-bar { background: #1a7a1a; height: 100%; width: 0%; transition: width 0.3s; }
.card { background: #1a1a1a; border-radius: 10px; padding: 20px; margin-top: 20px; }
.card h2 { font-size: 18px; margin-bottom: 10px; }
.meta { display: flex; flex-wrap: wrap; gap: 16px; font-size: 13px; color: #aaa; margin-bottom: 14px; }
.description {
font-size: 13px; color: #ccc; line-height: 1.65;
max-height: 120px; overflow: hidden; white-space: pre-wrap;
transition: max-height 0.3s ease;
}
.description.expanded { max-height: 3000px; }
.toggle-desc {
margin-top: 8px; background: none; border: none;
color: #3ea6ff; font-size: 13px; cursor: pointer;
}
.toggle-desc:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<!-- Top bar -->
<div class="topbar">
<a href="/" class="back-btn">← Назад</a>
</div>
<!-- Player -->
<div class="player-wrap">
<video id="player" controls autoplay preload="auto">
<source id="video-src"
src="/stream?format_id={{ default_format }}&url={{ youtube_url|urlencode }}"
type="video/mp4">
{% for sub in subtitles %}
<track id="track-{{ sub.lang }}" kind="subtitles"
label="{{ sub.label }}" srclang="{{ sub.lang }}"
src="/subtitles?url={{ youtube_url|urlencode }}&lang={{ sub.lang }}&auto={{ sub.auto }}">
{% endfor %}
</video>
</div>
<!-- Controls -->
<div class="controls">
<div>
<label>🎬 Качество</label>
<select id="quality-select">
{% for f in formats %}
<option value="{{ f.id }}" {% if f.id == default_format %}selected{% endif %}>
{{ f.label }}
</option>
{% endfor %}
</select>
</div>
<div>
<label>💬 Субтитры</label>
<select id="sub-select">
<option value="">Off</option>
{% for sub in subtitles %}
<option value="{{ sub.lang }}-{{ sub.auto }}">{{ sub.label }}</option>
{% endfor %}
</select>
</div>
<button class="btn" onclick="applySettings()">▶ Apply</button>
<!-- Download section -->
<div>
<label>⬇ Download</label>
<button class="dl-btn" id="dl-btn" onclick="startDownload()">
⬇ Download MP4
</button>
<div class="dl-status" id="dl-status"></div>
<!-- ✅ Remove the progress bar wrap entirely -->
</div>
</div>
<!-- Video info card -->
<div class="card">
<h2>{{ meta.title }}</h2>
<div class="meta">
<span>📺 {{ meta.uploader }}</span>
<span>👁 {{ meta.views }} views</span>
<span>⏱ {{ meta.duration }}</span>
<span>📅 {{ meta.upload_date }}</span>
</div>
<div class="description" id="desc">{{ meta.description }}</div>
<button class="toggle-desc" id="toggle-btn" onclick="toggleDesc()">▼ Show more</button>
</div>
</div>
<script>
const player = document.getElementById('player');
const videoSrc = document.getElementById('video-src');
const youtubeUrl = {{ youtube_url | tojson }};
function applySettings() {
const formatId = document.getElementById('quality-select').value;
const subVal = document.getElementById('sub-select').value;
const currentTime = player.currentTime;
videoSrc.src = `/stream?format_id=${formatId}&url=${encodeURIComponent(youtubeUrl)}`;
player.load();
player.currentTime = currentTime;
player.play();
for (let i = 0; i < player.textTracks.length; i++)
player.textTracks[i].mode = 'disabled';
if (subVal) {
const lang = subVal.split('-')[0];
const trackEl = document.getElementById(`track-${lang}`);
if (trackEl) trackEl.track.mode = 'showing';
}
}
function toggleDesc() {
const desc = document.getElementById('desc');
const btn = document.getElementById('toggle-btn');
desc.classList.toggle('expanded');
btn.textContent = desc.classList.contains('expanded') ? '▲ Show less' : '▼ Show more';
}
function startDownload() {
const formatId = document.getElementById('quality-select').value;
const status = document.getElementById('dl-status');
const dlBtn = document.getElementById('dl-btn');
// Show feedback so user knows something is happening
status.style.display = 'block';
status.textContent = '⏳ Starting download…';
dlBtn.textContent = '⏳ Downloading…';
dlBtn.classList.add('loading');
// ✅ Direct navigation — browser handles saving natively
window.location.href =
`/download?url=${encodeURIComponent(youtubeUrl)}&format_id=${formatId}`;
// Reset button after a moment — page doesn't reload on file downloads
setTimeout(() => {
dlBtn.textContent = '⬇ Download MP4';
dlBtn.classList.remove('loading');
status.textContent = '✅ Download started — check your browser downloads.';
setTimeout(() => {
status.style.display = 'none';
}, 4000);
}, 3000);
}
</script>
</body>
</html>

245
templates/Playlist.html Normal file
View 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 }} &nbsp;·&nbsp; {{ 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>