Compare commits
No commits in common. "a7d7d1cf504cda9bf71dd0d68618c9553d922f8a" and "6df26c12e6d2c9d310649244a328e7eae39c8be2" have entirely different histories.
a7d7d1cf50
...
6df26c12e6
41
Handoff.md
41
Handoff.md
@ -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 -->
|
|
||||||
@ -9,18 +9,9 @@ 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:
|
||||||
@ -32,14 +23,11 @@ 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_lines = [strip_html(l).strip() for l in lines[ts_idx + 1:]]
|
text = ' '.join(
|
||||||
if rolling:
|
strip_html(l).strip()
|
||||||
while text_lines and prev_last and text_lines[0] == prev_last:
|
for l in lines[ts_idx + 1:]
|
||||||
text_lines.pop(0)
|
if strip_html(l).strip()
|
||||||
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,18 +5,9 @@ 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;
|
||||||
@ -24,15 +15,11 @@ 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 textLines = lines.slice(tsIdx + 1).map(l => stripHtml(l).trim());
|
const text = lines
|
||||||
if (rolling) {
|
.slice(tsIdx + 1)
|
||||||
while (textLines.length && prevLast && textLines[0] === prevLast) {
|
.map(l => stripHtml(l).trim())
|
||||||
textLines.shift();
|
.filter(Boolean)
|
||||||
}
|
.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,18 +105,12 @@ 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