Step 5. A URL goes through the same pipeline as an upload — it just gains a download step and a source_url. - The host allowlist is checked on the parsed hostname before yt-dlp is invoked, so lookalikes and userinfo tricks are refused too - yt-dlp -J reads metadata synchronously with a 15s timeout; a timeout leaves the fields blank rather than failing the submission - Over-long tracks are refused from that metadata, before a byte is downloaded - Failed URL submissions offer Yritä uudelleen with the typed text intact; uploads cannot retry, so they offer re-upload Prefill takes track then title, and artist then creator then uploader, and leaves a field blank rather than inventing one. testdata/ytdlp-noose.json is a real dump of an ordinary upload, which has none of the music fields. Also fixes a URL-only submit being blocked by the file input's required attribute — HTML cannot express "one of these two", so the server says it. The image now takes yt-dlp from Alpine 3.24 instead of pip, which drops python3 and pip entirely; see decision 19.
72 lines
2.4 KiB
HTML
72 lines
2.4 KiB
HTML
{{define "content"}}
|
|
<h1>Lähetä kappale</h1>
|
|
|
|
{{with .Data.Error}}<p class="error">{{.}}</p>{{end}}
|
|
|
|
<form method="post" action="/submit" enctype="multipart/form-data" class="stack">
|
|
<label class="dropzone" id="dropzone" for="audio">
|
|
<input type="file" id="audio" name="audio" accept="audio/*">
|
|
<span class="dz-title">Raahaa äänitiedosto tähän</span>
|
|
<span class="muted small">tai valitse napsauttamalla</span>
|
|
<span class="filename" id="filename"></span>
|
|
</label>
|
|
<p class="or">tai</p>
|
|
<label>YouTube-linkki
|
|
<input type="url" name="url" placeholder="https://www.youtube.com/watch?v=…">
|
|
</label>
|
|
<button type="submit">Lähetä</button>
|
|
</form>
|
|
|
|
{{with .Data.Submissions}}
|
|
<section>
|
|
<h2>Omat lähetykset</h2>
|
|
<table>
|
|
<thead><tr><th>Kappale</th><th>Tila</th><th>Lähetetty</th></tr></thead>
|
|
<tbody>
|
|
{{range .}}
|
|
<tr>
|
|
<td><a href="/submit/{{.ID}}">{{if .Title}}{{.Title}}{{else}}(nimetön){{end}}</a></td>
|
|
<td>{{.Label}}</td>
|
|
<td class="nowrap">{{fidate .CreatedAt}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
<p class="muted small">Valmis lähetys odottaa Julkaise-painallusta — vasta se tuo kappaleen
|
|
muiden nähtäville.</p>
|
|
</section>
|
|
{{end}}
|
|
|
|
<p class="muted">Enintään 50 MB ja 15 minuuttia. Tiedosto muunnetaan Opus-muotoon, ja pääset
|
|
kirjoittamaan esittelyn odotellessa. Kappale julkaistaan vasta kun painat Julkaise.</p>
|
|
|
|
<script>
|
|
// The native control can't be relabelled or styled, so it is hidden behind this one — which
|
|
// still is the input, so keyboard focus and form submission work untouched.
|
|
(function () {
|
|
const zone = document.getElementById('dropzone')
|
|
const input = document.getElementById('audio')
|
|
const name = document.getElementById('filename')
|
|
|
|
const show = () => {
|
|
name.textContent = input.files.length ? input.files[0].name : ''
|
|
zone.classList.toggle('has-file', input.files.length > 0)
|
|
}
|
|
input.addEventListener('change', show)
|
|
|
|
for (const type of ['dragenter', 'dragover']) {
|
|
zone.addEventListener(type, e => { e.preventDefault(); zone.classList.add('over') })
|
|
}
|
|
for (const type of ['dragleave', 'dragend', 'drop']) {
|
|
zone.addEventListener(type, e => { e.preventDefault(); zone.classList.remove('over') })
|
|
}
|
|
zone.addEventListener('drop', e => {
|
|
if (e.dataTransfer.files.length) {
|
|
input.files = e.dataTransfer.files
|
|
show()
|
|
}
|
|
})
|
|
})()
|
|
</script>
|
|
{{end}}
|