253 lines
6.0 KiB
Python
253 lines
6.0 KiB
Python
from flask import Flask, render_template_string, request, redirect, url_for, flash
|
||
import os
|
||
|
||
from controller_core import enqueue_download
|
||
|
||
app = Flask(__name__)
|
||
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-secret")
|
||
|
||
HTML_FORM = """
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>YouTube Playlist Submit</title>
|
||
|
||
<!-- Make it behave on mobile -->
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||
|
||
<style>
|
||
:root {
|
||
color-scheme: dark;
|
||
}
|
||
|
||
body {
|
||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||
margin: 0;
|
||
padding: 0;
|
||
background: #020617;
|
||
color: #e5e7eb;
|
||
}
|
||
|
||
.page-wrapper {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 1.25rem;
|
||
}
|
||
|
||
.card {
|
||
width: 100%;
|
||
max-width: 520px;
|
||
background: rgba(15, 23, 42, 0.98);
|
||
padding: 1.75rem 1.5rem 1.4rem;
|
||
border-radius: 1rem;
|
||
box-shadow: 0 18px 45px rgba(0, 0, 0, 0.7);
|
||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||
}
|
||
|
||
h2 {
|
||
margin: 0 0 0.4rem;
|
||
font-size: 1.35rem;
|
||
}
|
||
|
||
.subtitle {
|
||
margin: 0 0 1.1rem;
|
||
font-size: 0.95rem;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.flash {
|
||
margin-bottom: 0.75rem;
|
||
padding: 0.55rem 0.75rem;
|
||
border-radius: 0.6rem;
|
||
background: rgba(30, 64, 175, 0.35);
|
||
border: 1px solid rgba(129, 140, 248, 0.7);
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 0.85rem;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin-bottom: 0.3rem;
|
||
font-size: 0.92rem;
|
||
}
|
||
|
||
input,
|
||
textarea {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 0.7rem 0.85rem;
|
||
border-radius: 0.65rem;
|
||
border: 1px solid #4b5563;
|
||
background: #020617;
|
||
color: #e5e7eb;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
input::placeholder,
|
||
textarea::placeholder {
|
||
color: #6b7280;
|
||
}
|
||
|
||
input:focus,
|
||
textarea:focus {
|
||
outline: 2px solid #38bdf8;
|
||
outline-offset: 2px;
|
||
border-color: #38bdf8;
|
||
}
|
||
|
||
textarea {
|
||
min-height: 3.2rem;
|
||
resize: vertical;
|
||
}
|
||
|
||
button {
|
||
width: 100%;
|
||
padding: 0.8rem 1.5rem;
|
||
border-radius: 999px;
|
||
border: none;
|
||
background: linear-gradient(135deg, #38bdf8, #6366f1);
|
||
color: #fff;
|
||
font-weight: 600;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-top: 0.2rem;
|
||
}
|
||
|
||
button:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.helper {
|
||
margin-top: 0.9rem;
|
||
font-size: 0.8rem;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.page-wrapper {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.card {
|
||
padding: 1.4rem 1.1rem 1.2rem;
|
||
border-radius: 0.9rem;
|
||
}
|
||
|
||
h2 {
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
input,
|
||
textarea {
|
||
font-size: 1.02rem;
|
||
}
|
||
|
||
button {
|
||
font-size: 1.02rem;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page-wrapper">
|
||
<div class="card">
|
||
<h2>Submit YouTube Playlist</h2>
|
||
<p class="subtitle">
|
||
Paste a playlist link below. It’ll be queued for download and assigned to the next free slot.
|
||
</p>
|
||
|
||
{% with messages = get_flashed_messages() %}
|
||
{% if messages %}
|
||
{% for m in messages %}
|
||
<div class="flash">{{ m }}</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
{% endwith %}
|
||
|
||
<form method="post" action="{{ url_for('submit') }}">
|
||
<div class="form-group">
|
||
<label for="url">YouTube URL</label>
|
||
<input
|
||
type="url"
|
||
id="url"
|
||
name="url"
|
||
required
|
||
placeholder="https://www.youtube.com/playlist?list=..."
|
||
>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="submitter">Your name (optional)</label>
|
||
<input
|
||
type="text"
|
||
id="submitter"
|
||
name="submitter"
|
||
placeholder="Simon Peter"
|
||
>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="note">Note (optional)</label>
|
||
<textarea
|
||
id="note"
|
||
name="note"
|
||
rows="3"
|
||
placeholder="Service, event, etc."
|
||
></textarea>
|
||
</div>
|
||
|
||
<button type="submit">Submit Playlist</button>
|
||
</form>
|
||
|
||
<p class="helper">
|
||
Playlists are kept for roughly 48 hours after they’re downloaded.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
@app.route("/", methods=["GET"])
|
||
def index():
|
||
return render_template_string(HTML_FORM)
|
||
|
||
|
||
@app.route("/submit", methods=["POST"])
|
||
def submit():
|
||
url = request.form.get("url", "").strip()
|
||
submitter = request.form.get("submitter", "").strip() or None
|
||
note = request.form.get("note", "").strip() or None
|
||
|
||
if not url.startswith("http"):
|
||
flash("Invalid URL.")
|
||
return redirect(url_for("index"))
|
||
|
||
result = enqueue_download(url, submitter, note)
|
||
|
||
if result.get("status") == "queue_full":
|
||
secs = result.get("oldest_expires_in_seconds", 0)
|
||
hours = max(1, secs // 3600)
|
||
flash(f"Queue is full. Oldest playlist expires in about {hours} hour(s).")
|
||
else:
|
||
slot = result.get("slot")
|
||
msg = f"Playlist queued in slot {slot}."
|
||
if result.get("reused_slot"):
|
||
msg += " (Existing slot – only new videos will be added.)"
|
||
else:
|
||
msg += " It will start downloading shortly."
|
||
flash(msg)
|
||
|
||
return redirect(url_for("index"))
|
||
|
||
if __name__ == "__main__":
|
||
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7531))) |