Compare commits

..

No commits in common. "a7d7d1cf504cda9bf71dd0d68618c9553d922f8a" and "6df26c12e6d2c9d310649244a328e7eae39c8be2" have entirely different histories.

4 changed files with 10 additions and 82 deletions

View File

@ -1,41 +0,0 @@
# 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 -->

View File

@ -9,18 +9,9 @@ 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:
@ -32,14 +23,11 @@ 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_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)
text = ' '.join(
strip_html(l).strip()
for l in lines[ts_idx + 1:]
if strip_html(l).strip()
)
if text:
cues.append(text)
return cues

View File

@ -5,18 +5,9 @@ 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;
@ -24,15 +15,11 @@ 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 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(' ');
const text = lines
.slice(tsIdx + 1)
.map(l => stripHtml(l).trim())
.filter(Boolean)
.join(' ');
if (text) cues.push(text);
}
return cues;

View File

@ -105,18 +105,12 @@ 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) {