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)
|
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):
|
def parse_vtt(content):
|
||||||
|
rolling = has_inline_timing_tags(content)
|
||||||
blocks = re.split(r'\n{2,}', content.strip())
|
blocks = re.split(r'\n{2,}', content.strip())
|
||||||
cues = []
|
cues = []
|
||||||
|
prev_last = None
|
||||||
for block in blocks:
|
for block in blocks:
|
||||||
lines = block.strip().splitlines()
|
lines = block.strip().splitlines()
|
||||||
if not lines:
|
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)
|
ts_idx = next((i for i, l in enumerate(lines) if '-->' in l), None)
|
||||||
if ts_idx is None:
|
if ts_idx is None:
|
||||||
continue
|
continue
|
||||||
text = ' '.join(
|
text_lines = [strip_html(l).strip() for l in lines[ts_idx + 1:]]
|
||||||
strip_html(l).strip()
|
if rolling:
|
||||||
for l in lines[ts_idx + 1:]
|
while text_lines and prev_last and text_lines[0] == prev_last:
|
||||||
if strip_html(l).strip()
|
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:
|
if text:
|
||||||
cues.append(text)
|
cues.append(text)
|
||||||
return cues
|
return cues
|
||||||
|
|||||||
@ -5,9 +5,18 @@ function stripHtml(text) {
|
|||||||
return text.replace(/<[^>]+>/g, '');
|
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) {
|
function parseVtt(content) {
|
||||||
|
const rolling = hasInlineTimingTags(content);
|
||||||
const blocks = content.split(/\n{2,}/);
|
const blocks = content.split(/\n{2,}/);
|
||||||
const cues = [];
|
const cues = [];
|
||||||
|
let prevLast = null;
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
const lines = block.trim().split('\n');
|
const lines = block.trim().split('\n');
|
||||||
if (!lines.length) continue;
|
if (!lines.length) continue;
|
||||||
@ -15,11 +24,15 @@ function parseVtt(content) {
|
|||||||
if (lines[0].startsWith('NOTE') || lines[0].startsWith('STYLE')) continue;
|
if (lines[0].startsWith('NOTE') || lines[0].startsWith('STYLE')) continue;
|
||||||
const tsIdx = lines.findIndex(l => l.includes('-->'));
|
const tsIdx = lines.findIndex(l => l.includes('-->'));
|
||||||
if (tsIdx === -1) continue;
|
if (tsIdx === -1) continue;
|
||||||
const text = lines
|
const textLines = lines.slice(tsIdx + 1).map(l => stripHtml(l).trim());
|
||||||
.slice(tsIdx + 1)
|
if (rolling) {
|
||||||
.map(l => stripHtml(l).trim())
|
while (textLines.length && prevLast && textLines[0] === prevLast) {
|
||||||
.filter(Boolean)
|
textLines.shift();
|
||||||
.join(' ');
|
}
|
||||||
|
}
|
||||||
|
const nonEmpty = textLines.filter(Boolean);
|
||||||
|
if (rolling && nonEmpty.length) prevLast = nonEmpty[nonEmpty.length - 1];
|
||||||
|
const text = nonEmpty.join(' ');
|
||||||
if (text) cues.push(text);
|
if (text) cues.push(text);
|
||||||
}
|
}
|
||||||
return cues;
|
return cues;
|
||||||
|
|||||||
@ -105,12 +105,18 @@ async function runDownload(videoUrl, audioOnly, send, signal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const writtenMd = new Set();
|
||||||
for (const subPath of subtitlePaths) {
|
for (const subPath of subtitlePaths) {
|
||||||
if (!existsSync(subPath)) continue;
|
if (!existsSync(subPath)) continue;
|
||||||
send('info', `Converting: ${basename(subPath)}`);
|
send('info', `Converting: ${basename(subPath)}`);
|
||||||
try {
|
try {
|
||||||
const md = convertSubtitleToMarkdown(subPath);
|
const md = convertSubtitleToMarkdown(subPath);
|
||||||
const mdPath = subPath.replace(/\.(vtt|srt)$/, '.md');
|
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');
|
writeFileSync(mdPath, md, 'utf-8');
|
||||||
send('info', `Saved: ${basename(mdPath)}`);
|
send('info', `Saved: ${basename(mdPath)}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user