Compare commits
3 Commits
6df26c12e6
...
a7d7d1cf50
| Author | SHA1 | Date | |
|---|---|---|---|
| a7d7d1cf50 | |||
| 03eec79b4c | |||
| 361c88be65 |
41
Handoff.md
Normal file
41
Handoff.md
Normal file
@ -0,0 +1,41 @@
|
||||
# yt-dlf — Handoff
|
||||
|
||||
> Übergabedokument für die Fortsetzung in einer Claude-Code-Session.
|
||||
> Stand: 2026-08-29 — Web-Frontend für yt-dlp/ffmpeg mit Login, Ad-Spalten
|
||||
> und User-Verwaltung; implementiert, Deploy-Status unbekannt.
|
||||
> Dauerhafte Fakten stehen in `README.md`/`INSTALL.md`; hier steht nur, was
|
||||
> **nicht** aus Code und Git-Historie ersichtlich ist.
|
||||
|
||||
**🔜 NÄCHSTE SESSION:** kein vorgeplanter Auftrag
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- [ ] Deploy-Status klären — `build/` und `users.txt` liegen lokal vor
|
||||
(beide gitignored), aber ob und wo die App produktiv läuft, ist nirgends
|
||||
festgehalten.
|
||||
|
||||
## Stolperfallen
|
||||
|
||||
- **Git-Konten-Mix** — Bestandsprojekt: Remote `origin` zeigt direkt auf
|
||||
`git@gitea.101010.cloud:stwaidele/yt-dlf.git` und die Repo-Config steht
|
||||
auf Stefans Konto. Claude-Commits daher per Author-Override
|
||||
(`git -c user.name="Claude <Modell>" -c user.email="<slug>@waidele.info" commit …`),
|
||||
Repo-Config nicht umbiegen. Push läuft über Stefans Default-Key.
|
||||
|
||||
## Session 2026-08-29 — Handoff angelegt (Claude Fable 5)
|
||||
|
||||
Projekt hatte noch keine `Handoff.md`; per `/handoff init` aus README,
|
||||
INSTALL.md und Git-Historie erstellt. Kein Projekt-`CLAUDE.md` vorhanden.
|
||||
Beide bisherigen Commits (16.05.) stammen von Stefan; seither keine
|
||||
Änderungen im Working Tree.
|
||||
|
||||
## Typische Handgriffe
|
||||
|
||||
Dev-Server, Deployment und User-Verwaltung sind vollständig in `README.md`
|
||||
und `INSTALL.md` dokumentiert — dort nachschlagen, hier nicht duplizieren.
|
||||
|
||||
## Verwandte Handoffs
|
||||
|
||||
_keine_
|
||||
|
||||
<!-- Ältere Sessions: git log -p Handoff.md -->
|
||||
@ -9,9 +9,18 @@ def strip_html(text):
|
||||
return re.sub(r'<[^>]+>', '', text)
|
||||
|
||||
|
||||
# YouTube auto-captions use rolling cues: each cue repeats the previous
|
||||
# cue's last line as plain text before the new text (which usually, but not
|
||||
# always, carries inline word timings like <00:00:00.200> or <c>…</c>).
|
||||
def has_inline_timing_tags(text):
|
||||
return re.search(r'<\d{2}:\d{2}:|<c[.\w]*>', text) is not None
|
||||
|
||||
|
||||
def parse_vtt(content):
|
||||
rolling = has_inline_timing_tags(content)
|
||||
blocks = re.split(r'\n{2,}', content.strip())
|
||||
cues = []
|
||||
prev_last = None
|
||||
for block in blocks:
|
||||
lines = block.strip().splitlines()
|
||||
if not lines:
|
||||
@ -23,11 +32,14 @@ def parse_vtt(content):
|
||||
ts_idx = next((i for i, l in enumerate(lines) if '-->' in l), None)
|
||||
if ts_idx is None:
|
||||
continue
|
||||
text = ' '.join(
|
||||
strip_html(l).strip()
|
||||
for l in lines[ts_idx + 1:]
|
||||
if strip_html(l).strip()
|
||||
)
|
||||
text_lines = [strip_html(l).strip() for l in lines[ts_idx + 1:]]
|
||||
if rolling:
|
||||
while text_lines and prev_last and text_lines[0] == prev_last:
|
||||
text_lines.pop(0)
|
||||
non_empty = [l for l in text_lines if l]
|
||||
if rolling and non_empty:
|
||||
prev_last = non_empty[-1]
|
||||
text = ' '.join(non_empty)
|
||||
if text:
|
||||
cues.append(text)
|
||||
return cues
|
||||
|
||||
@ -5,9 +5,18 @@ function stripHtml(text) {
|
||||
return text.replace(/<[^>]+>/g, '');
|
||||
}
|
||||
|
||||
// YouTube auto-captions use rolling cues: each cue repeats the previous
|
||||
// cue's last line as plain text before the new text (which usually, but not
|
||||
// always, carries inline word timings like <00:00:00.200> or <c>…</c>).
|
||||
function hasInlineTimingTags(text) {
|
||||
return /<\d{2}:\d{2}:|<c[.\w]*>/.test(text);
|
||||
}
|
||||
|
||||
function parseVtt(content) {
|
||||
const rolling = hasInlineTimingTags(content);
|
||||
const blocks = content.split(/\n{2,}/);
|
||||
const cues = [];
|
||||
let prevLast = null;
|
||||
for (const block of blocks) {
|
||||
const lines = block.trim().split('\n');
|
||||
if (!lines.length) continue;
|
||||
@ -15,11 +24,15 @@ function parseVtt(content) {
|
||||
if (lines[0].startsWith('NOTE') || lines[0].startsWith('STYLE')) continue;
|
||||
const tsIdx = lines.findIndex(l => l.includes('-->'));
|
||||
if (tsIdx === -1) continue;
|
||||
const text = lines
|
||||
.slice(tsIdx + 1)
|
||||
.map(l => stripHtml(l).trim())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const textLines = lines.slice(tsIdx + 1).map(l => stripHtml(l).trim());
|
||||
if (rolling) {
|
||||
while (textLines.length && prevLast && textLines[0] === prevLast) {
|
||||
textLines.shift();
|
||||
}
|
||||
}
|
||||
const nonEmpty = textLines.filter(Boolean);
|
||||
if (rolling && nonEmpty.length) prevLast = nonEmpty[nonEmpty.length - 1];
|
||||
const text = nonEmpty.join(' ');
|
||||
if (text) cues.push(text);
|
||||
}
|
||||
return cues;
|
||||
|
||||
@ -105,12 +105,18 @@ async function runDownload(videoUrl, audioOnly, send, signal) {
|
||||
}
|
||||
}
|
||||
|
||||
const writtenMd = new Set();
|
||||
for (const subPath of subtitlePaths) {
|
||||
if (!existsSync(subPath)) continue;
|
||||
send('info', `Converting: ${basename(subPath)}`);
|
||||
try {
|
||||
const md = convertSubtitleToMarkdown(subPath);
|
||||
const mdPath = subPath.replace(/\.(vtt|srt)$/, '.md');
|
||||
if (writtenMd.has(md)) {
|
||||
send('info', `Skipped (identical): ${basename(mdPath)}`);
|
||||
continue;
|
||||
}
|
||||
writtenMd.add(md);
|
||||
writeFileSync(mdPath, md, 'utf-8');
|
||||
send('info', `Saved: ${basename(mdPath)}`);
|
||||
} catch (err) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user