From 9c7033d1b4df13c89fb5a89e6a9c15602afd2654 Mon Sep 17 00:00:00 2001 From: Trey t Date: Thu, 23 Apr 2026 07:07:41 -0500 Subject: [PATCH] Add curated-videos markdown report + generator script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit youtube_videos.md lists every entry in youtube_videos.json with its tense-guide / grammar-note id, title, channel, upload date, duration, views, and likes (where public). Also flags the two topics with no curated video so the gap is auditable in one place. generate_videos_markdown.py queries yt-dlp in parallel for each unique videoId and writes the markdown. Rerun when curation changes. One current entry (saber-vs-conocer → j87i7MVCvIE) is now marked Private Video — needs re-curation as a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- Conjuga/Conjuga/youtube_videos.md | 89 +++++++ Conjuga/Scripts/generate_videos_markdown.py | 259 ++++++++++++++++++++ 2 files changed, 348 insertions(+) create mode 100644 Conjuga/Conjuga/youtube_videos.md create mode 100644 Conjuga/Scripts/generate_videos_markdown.py diff --git a/Conjuga/Conjuga/youtube_videos.md b/Conjuga/Conjuga/youtube_videos.md new file mode 100644 index 0000000..8d87556 --- /dev/null +++ b/Conjuga/Conjuga/youtube_videos.md @@ -0,0 +1,89 @@ +# Curated YouTube Videos + +Every tense guide and grammar note in the app can be tied to a single curated YouTube video. This file is generated from `Conjuga/youtube_videos.json` by `Scripts/generate_videos_markdown.py` — regenerate when you add or change entries. + +- Total tense-guide entries: **19** of 20 +- Total grammar-note entries: **35** of 36 +- Last verified: **2026-04-23** (run `python3 Scripts/generate_videos_markdown.py` to refresh) + +Like counts are often blank because YouTube hides the public count on most videos for signed-out requests. Titles and durations are pulled live from YouTube; unavailable entries mean the video has been taken down, made private, or region-locked. + +## Tense guides + +Tied to `TenseGuide.tenseId` in the Guide tab. + +| Tense ID | Title | Channel | Uploaded | Duration | Views | Likes | URL | +|---|---|---|---|---|---|---|---| +| `ind_presente` | Spanish Present Tense: Regular -AR -ER -IR verb conjugation | Learn Spanish with Spanish55 | 2019-10-18 | 7:14 | 51,288 | 233 | [watch](https://www.youtube.com/watch?v=8HWXJjxvOTE) | +| `ind_preterito` | Preterite/pretérito in Spanish: how to form it & learn it! Easy animated explanation for beginners. | Lingo Learner | 2018-01-08 | 8:41 | 427,420 | 5,858 | [watch](https://www.youtube.com/watch?v=R4SiKCStHuU) | +| `ind_imperfecto` | Spanish Imperfect Tense Tutorial v2.0 | Cyber Profe | 2022-12-30 | 7:29 | 23,872 | 111 | [watch](https://www.youtube.com/watch?v=hMg05drgI7w) | +| `ind_futuro` | Regular, Future Tense Conjugation in Spanish (w/ Ser, Estar & Ir) | The Spanish Dude | 2021-04-21 | 10:09 | 22,610 | 729 | [watch](https://www.youtube.com/watch?v=yjQGJFCUOog) | +| `ind_perfecto` | Forming the PRESENT PERFECT in Spanish (PRESENTE PERFECTO) | MaestroKaplan | 2012-05-27 | 5:14 | 234,626 | 1,422 | [watch](https://www.youtube.com/watch?v=y_yeb6qkMbs) | +| `ind_pluscuamperfecto` | Past Perfect, Pluperfect, Pluscuamperfecto in Spanish | MaestroKaplan | 2012-08-14 | 4:56 | 142,175 | 835 | [watch](https://www.youtube.com/watch?v=5VpGDhJ8eNw) | +| `ind_futuro_perfecto` | FUTURE PERFECT: How to form / conjugate verbs in the futuro perfecto in Spanish | MaestroKaplan | 2012-07-10 | 4:01 | 49,213 | 290 | [watch](https://www.youtube.com/watch?v=459J8Cy-9DU) | +| `cond_presente` | 03 Spanish Lesson - Conditional Tense | Señor Jordan | 2012-08-11 | 12:23 | 230,409 | 1,248 | [watch](https://www.youtube.com/watch?v=9ctJ6I-4NJ8) | +| `cond_perfecto` | How to form the CONDITIONAL PERFECT in Spanish (condicional perfecto) | MaestroKaplan | 2012-06-22 | 4:04 | 36,008 | 227 | [watch](https://www.youtube.com/watch?v=jTBATres2hw) | +| `subj_presente` | The Subjunctive in Spanish \| The Language Tutor *Lesson 58* | The Language Tutor - Spanish | 2020-02-23 | 16:30 | 424,782 | 11,272 | [watch](https://www.youtube.com/watch?v=CRvXpo45oHw) | +| `subj_imperfecto_1` | Easily conquer the Spanish Imperfect Subjunctive | Anytime Español | 2024-04-14 | 6:34 | 17,857 | 706 | [watch](https://www.youtube.com/watch?v=oqMCJORRdVs) | +| `subj_imperfecto_2` | Easily conquer the Spanish Imperfect Subjunctive | Anytime Español | 2024-04-14 | 6:34 | 17,857 | 706 | [watch](https://www.youtube.com/watch?v=oqMCJORRdVs) | +| `subj_perfecto` | Present Perfect Subjunctive Spanish Guide: How to Use "Haya" in Spanish | Anytime Español | 2025-05-18 | 10:53 | 4,403 | 401 | [watch](https://www.youtube.com/watch?v=gAgFFpt6-08) | +| `subj_pluscuamperfecto_1` | The Past Perfect or Pluperfect Subjunctive in Spanish: Forms and Uses | Professor Jason Spanish and Portuguese | 2014-05-09 | 22:47 | 72,339 | 1,011 | [watch](https://www.youtube.com/watch?v=aAQCodqWhkU) | +| `subj_pluscuamperfecto_2` | The Past Perfect or Pluperfect Subjunctive in Spanish: Forms and Uses | Professor Jason Spanish and Portuguese | 2014-05-09 | 22:47 | 72,339 | 1,011 | [watch](https://www.youtube.com/watch?v=aAQCodqWhkU) | +| `subj_futuro` | Spanish Answers, Episode 10: Future Subjunctive | Spanish Answers | 2019-05-14 | 12:52 | 2,580 | 60 | [watch](https://www.youtube.com/watch?v=YPWJsmD3hN4) | +| `subj_futuro_perfecto` | Free Spanish Lessons 151-Spanish Subjunctive tense:Future Perfect-Video 1/2 | Aprender Idiomas y Cultura General con Rodrigo | 2014-05-18 | 10:00 | 2,025 | 20 | [watch](https://www.youtube.com/watch?v=9vmo2C-0iuQ) | +| `imp_afirmativo` | Commands in Spanish: The Imperative Mood Explained | BaseLang | 2021-12-30 | 9:47 | 80,093 | — | [watch](https://www.youtube.com/watch?v=uQi14msiaYg) | +| `imp_negativo` | The Negative Imperative in Spanish | CENTRO IDEAL TUTORING | 2024-01-09 | 20:29 | 108 | 5 | [watch](https://www.youtube.com/watch?v=wsLFs_OQOfM) | + +## Grammar notes + +Tied to `GrammarNote.id` (hand-authored + generated) in the Guide → Grammar tab. + +| Grammar ID | Title | Channel | Uploaded | Duration | Views | Likes | URL | +|---|---|---|---|---|---|---|---| +| `ser-vs-estar` | SER vs. ESTAR - The COMPLETE guide 🇪🇸 \| How to Use "To Be" in Spanish Correctly | My Daily Spanish | 2024-06-01 | 12:34 | 106,861 | 6,059 | [watch](https://www.youtube.com/watch?v=X-7k7R3Ca9U) | +| `por-vs-para` | Por vs Para 🇪🇸 The definitive guide! 🤓 | Spanish with James | 2026-02-05 | 15:07 | 3,115 | 124 | [watch](https://www.youtube.com/watch?v=PX6wnebioOA) | +| `preterite-vs-imperfect` | Spanish Past Tenses 101: Preterite vs Imperfect | Tell Me In Spanish | 2024-10-14 | 21:54 | 49,456 | 1,623 | [watch](https://www.youtube.com/watch?v=DfrpSIAuUjg) | +| `subjunctive-triggers` | Spanish Subjunctive Part 2/5: Wishes, Emotions & Doubt (WEIRDO Triggers) | NOT Your Spanish Professor | 2026-02-08 | 24:25 | 412 | 23 | [watch](https://www.youtube.com/watch?v=OzGWFJTcrKc) | +| `reflexive-verbs` | Spanish Reflexive Verbs: How-To, 20 Verbs & My 1 RULE to make them EASY | Tell Me In Spanish | 2024-06-24 | 16:57 | 29,656 | 912 | [watch](https://www.youtube.com/watch?v=z2UXjjp3vnI) | +| `object-pronouns` | DIRECT & INDIRECT OBJECT PRONOUNS in Spanish: ALL you need to know – me, te, lo, la, nos, los... | Butterfly Spanish | 2020-01-27 | 33:55 | 588,743 | 16,646 | [watch](https://www.youtube.com/watch?v=vJD6AeHZ0j4) | +| `gustar-like-verbs` | How Verbs Like Gustar Work: Never Confuse Them Again | Tell Me In Spanish | 2023-04-24 | 9:48 | 5,272 | 205 | [watch](https://www.youtube.com/watch?v=eCDWXZlDHUA) | +| `comparatives-superlatives` | Learn the COMPARATIVE and SUPERLATIVE in Spanish | Tommys Spanish School | 2018-06-16 | 5:33 | 60,589 | 1,134 | [watch](https://www.youtube.com/watch?v=OSxtLNHaRQg) | +| `conditional-if-clauses` | Si Clauses: The Spanish Hypothetical Explained | BaseLang | 2022-05-13 | 8:59 | 16,328 | — | [watch](https://www.youtube.com/watch?v=thvW8qVsqkE) | +| `commands-imperative` | Commands in Spanish: The Imperative Mood Explained | BaseLang | 2021-12-30 | 9:47 | 80,093 | — | [watch](https://www.youtube.com/watch?v=uQi14msiaYg) | +| `saber-vs-conocer` | _(unavailable — Private video)_ | — | — | — | — | — | [watch](https://www.youtube.com/watch?v=j87i7MVCvIE) | +| `double-negatives` | Learn Spanish Grammar: Double Negatives in Spanish – No, Nada, Niguno, Ninguna, Nadie & more | Butterfly Spanish | 2017-05-29 | 25:29 | 229,767 | 5,896 | [watch](https://www.youtube.com/watch?v=dmcLNMYxMFI) | +| `adjective-placement` | SPANISH ADJECTIVES: BEFORE or AFTER NOUNS?? (English audio) | Speak Spanish With Paula | 2021-07-30 | 12:10 | 21,668 | 982 | [watch](https://www.youtube.com/watch?v=JNh6nuZe_zo) | +| `tener-expressions` | Idiomatic Expressions with TENER | MaestroKaplan | 2012-06-05 | 5:53 | 70,535 | 219 | [watch](https://www.youtube.com/watch?v=uD1rcv_ZTNA) | +| `personal-a` | Personal 'A' in Spanish: What is it & How to Use it | Tell Me In Spanish | 2023-07-27 | 7:32 | 5,889 | 277 | [watch](https://www.youtube.com/watch?v=5QRZ13VZ2PE) | +| `relative-pronouns` | Master Spanish Relative Pronouns: Where, When, How, Who, and Whose A1 B2 | Spanish Pro | 2024-12-01 | 8:17 | 211 | 17 | [watch](https://www.youtube.com/watch?v=2YmFy5sJOj8) | +| `future-vs-ir-a` | IR A + Infinitive VS. Future Tense: What's the difference in Spanish? | Learn Spanish with SpanishPod101.com | 2020-10-21 | 11:15 | 7,037 | 244 | [watch](https://www.youtube.com/watch?v=oGHz-O_m0tk) | +| `accent-marks-stress` | LA TILDE \| Word Stress and Accent Marks in Spanish \| Stressing the Right Syllable in Spanish Words! | Spanish Like a Native | 2020-09-15 | 8:44 | 5,796 | 195 | [watch](https://www.youtube.com/watch?v=iBWTR-a3pZc) | +| `se-constructions` | Understanding "SE" in Spanish: Reflexive, Passive, and Impersonal Constructions EXPLAINED | Learn Spanish with Spanish55 | 2024-08-16 | 3:57 | 1,271 | 61 | [watch](https://www.youtube.com/watch?v=ndxsrGD7b-8) | +| `spanish-suffixes` | How to use Suffixes in Spanish - Basic Grammar | Learn Spanish with SpanishPod101.com | 2021-10-06 | 14:45 | 4,059 | 201 | [watch](https://www.youtube.com/watch?v=2acPjFrmJCc) | +| `common-irregular-verbs` | Master the 4 Most Important Irregular Verbs in Spanish (SER, ESTAR, TENER, IR) | Mr. Spanish Channel - Señor Español | 2025-10-27 | 19:13 | 1,659 | 121 | [watch](https://www.youtube.com/watch?v=1CmeCwO0t5w) | +| `types-of-irregular-verbs` | Stem-Changing Verbs in Spanish: 90% of "Irregular" Verbs Solved | Tell Me In Spanish | 2024-04-01 | 11:12 | 10,276 | 314 | [watch](https://www.youtube.com/watch?v=tQuQcuwsIqw) | +| `present-indicative-conjugation` | Spanish Present Tense: Regular -AR -ER -IR verb conjugation | Learn Spanish with Spanish55 | 2019-10-18 | 7:14 | 51,288 | 233 | [watch](https://www.youtube.com/watch?v=8HWXJjxvOTE) | +| `articles-and-gender` | Definite Articles in Spanish: Rules and Examples | Spanish Learning Lab | 2018-09-06 | 6:33 | 23,423 | 240 | [watch](https://www.youtube.com/watch?v=h2b37zYtQuc) | +| `possessive-adjectives` | Possessive adjectives in Spanish for beginners: how to say my, your, his, her, their explanation | Lingo Learner | 2020-11-13 | 5:14 | 122,156 | 1,904 | [watch](https://www.youtube.com/watch?v=zJQxR4mUj2Y) | +| `demonstrative-adjectives` | THIS & THAT in Spanish: How to use ESTE, ESE, AQUEL (Demonstrative Adjectives) | Spring Spanish - Learn Spanish with Chunks | 2021-09-10 | 12:35 | 16,728 | 537 | [watch](https://www.youtube.com/watch?v=jZJ0tE3WZlo) | +| `greetings-farewells` | Every Spanish Greeting You Need (Formal, Casual & Slang) | The Language Tutor - Spanish | 2019-03-10 | 9:54 | 673,750 | 17,161 | [watch](https://www.youtube.com/watch?v=AqfQQZVmTUw) | +| `poder-infinitive` | Spanish - The Verb “Poder“ Explained In 3 Minutes | TheLanguageBro | 2023-07-01 | 3:16 | 4,913 | 106 | [watch](https://www.youtube.com/watch?v=hCUbz5942EY) | +| `al-del-contractions` | Spanish Contractions AL and DEL \| The Language Tutor * Lesson 15 * | The Language Tutor - Spanish | 2019-04-28 | 3:34 | 211,085 | 5,457 | [watch](https://www.youtube.com/watch?v=nWPZZWIwWxg) | +| `prepositional-pronouns` | PREPOSITIONAL PRONOUNS: How and when to use them in Spanish | MaestroKaplan | 2012-06-08 | 3:51 | 37,809 | 152 | [watch](https://www.youtube.com/watch?v=l29XtaZSSyY) | +| `irregular-yo-verbs` | Spanish Irregular Yo Form Verbs Go Go Verbs Song | ilovecomputers123 | 2012-05-05 | 1:58 | 118,032 | 403 | [watch](https://www.youtube.com/watch?v=yRf6adUKSzQ) | +| `stem-changing-verbs` | Stem-Changing Verbs in Spanish: 90% of "Irregular" Verbs Solved | Tell Me In Spanish | 2024-04-01 | 11:12 | 10,276 | 314 | [watch](https://www.youtube.com/watch?v=tQuQcuwsIqw) | +| `stressed-possessives` | Spanish Long Form Possessive Adjectives Grammar \| Possessive Pronouns | Mastny Spanish Academy | 2023-04-21 | 8:06 | 5,049 | 32 | [watch](https://www.youtube.com/watch?v=epObIkGAPoU) | +| `present-perfect-tense` | Forming the PRESENT PERFECT in Spanish (PRESENTE PERFECTO) | MaestroKaplan | 2012-05-27 | 5:14 | 234,626 | 1,422 | [watch](https://www.youtube.com/watch?v=y_yeb6qkMbs) | +| `future-perfect-tense` | FUTURE PERFECT: How to form / conjugate verbs in the futuro perfecto in Spanish | MaestroKaplan | 2012-07-10 | 4:01 | 49,213 | 290 | [watch](https://www.youtube.com/watch?v=459J8Cy-9DU) | + +## Topics without a curated video + +These show a "No video yet" label in the app. Add entries to `Conjuga/youtube_videos.json` to fill them in. + +**Tense guides:** + +- `ind_preterito_anterior` + +**Grammar notes:** + +- `estar-gerund-progressive` diff --git a/Conjuga/Scripts/generate_videos_markdown.py b/Conjuga/Scripts/generate_videos_markdown.py new file mode 100644 index 0000000..4597af0 --- /dev/null +++ b/Conjuga/Scripts/generate_videos_markdown.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +"""Generate a markdown report of every curated YouTube video referenced by the app. + +Reads Conjuga/youtube_videos.json, queries yt-dlp for metadata on each video, +and emits Conjuga/youtube_videos.md with tables for tense guides and grammar +notes plus a list of topics with no curated video. + +Usage: + python3 Scripts/generate_videos_markdown.py + +Requires `yt-dlp` on PATH. Videos that have been taken down or made private +appear in the tables with an "(unavailable)" marker in the title column. +""" + +from __future__ import annotations + +import json +import re +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import date +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +VIDEOS_JSON = REPO_ROOT / "Conjuga" / "youtube_videos.json" +OUTPUT_MD = REPO_ROOT / "Conjuga" / "youtube_videos.md" + +# The curated ids we expect — anything in the source file that's missing from +# the JSON shows up in the "missing" section at the bottom. +EXPECTED_TENSE_IDS = [ + "ind_presente", "ind_preterito", "ind_imperfecto", "ind_futuro", + "ind_perfecto", "ind_pluscuamperfecto", "ind_futuro_perfecto", + "ind_preterito_anterior", + "cond_presente", "cond_perfecto", + "subj_presente", "subj_imperfecto_1", "subj_imperfecto_2", + "subj_perfecto", "subj_pluscuamperfecto_1", "subj_pluscuamperfecto_2", + "subj_futuro", "subj_futuro_perfecto", + "imp_afirmativo", "imp_negativo", +] + +EXPECTED_GRAMMAR_IDS = [ + "ser-vs-estar", "por-vs-para", "preterite-vs-imperfect", + "subjunctive-triggers", "reflexive-verbs", "object-pronouns", + "gustar-like-verbs", "comparatives-superlatives", + "conditional-if-clauses", "commands-imperative", "saber-vs-conocer", + "double-negatives", "adjective-placement", "tener-expressions", + "personal-a", "relative-pronouns", "future-vs-ir-a", + "accent-marks-stress", "se-constructions", "estar-gerund-progressive", + "spanish-suffixes", "common-irregular-verbs", "types-of-irregular-verbs", + "present-indicative-conjugation", "articles-and-gender", + "possessive-adjectives", "demonstrative-adjectives", + "greetings-farewells", "poder-infinitive", "al-del-contractions", + "prepositional-pronouns", "irregular-yo-verbs", "stem-changing-verbs", + "stressed-possessives", "present-perfect-tense", "future-perfect-tense", +] + + +def fetch_metadata(video_id: str) -> dict: + """Return a dict of useful metadata fields for a single video. + + On any yt-dlp failure (video removed, network issue, extraction break) + returns a dict with `unavailable=True` so the caller can mark the row. + """ + try: + result = subprocess.run( + ["yt-dlp", "--skip-download", "--dump-json", "--no-warnings", "--", video_id], + capture_output=True, + text=True, + timeout=30, + ) + except subprocess.TimeoutExpired: + return {"unavailable": True, "reason": "timeout"} + + if result.returncode != 0: + # yt-dlp errors look like: + # "ERROR: [youtube] ID: . " + # Extract just and drop everything after the first "." so the + # markdown table stays readable. Help URLs contain colons so a naive + # split-on-colon grabs the wrong chunk. + reason = "yt-dlp failed" + pattern = re.compile(r"ERROR:\s*\[[^\]]+\]\s*[^:]+:\s*(.+)") + for line in result.stderr.strip().splitlines(): + m = pattern.search(line) + if m: + reason = m.group(1).split(". ")[0].rstrip(".") + break + return {"unavailable": True, "reason": reason} + + try: + data = json.loads(result.stdout) + except json.JSONDecodeError: + return {"unavailable": True, "reason": "invalid json"} + + return { + "unavailable": False, + "title": data.get("title") or "", + "uploader": data.get("uploader") or data.get("channel") or "", + "upload_date": data.get("upload_date") or "", # YYYYMMDD + "duration": data.get("duration"), # seconds + "view_count": data.get("view_count"), + "like_count": data.get("like_count"), + } + + +def fmt_duration(seconds: int | None) -> str: + if not seconds: + return "—" + h, rem = divmod(int(seconds), 3600) + m, s = divmod(rem, 60) + if h: + return f"{h}:{m:02d}:{s:02d}" + return f"{m}:{s:02d}" + + +def fmt_date(raw: str) -> str: + if not raw or len(raw) != 8: + return "—" + return f"{raw[0:4]}-{raw[4:6]}-{raw[6:8]}" + + +def fmt_int(n: int | None) -> str: + if n is None: + return "—" + return f"{n:,}" + + +def render_row(key: str, curated: dict, meta: dict) -> str: + video_id = curated["videoId"] + url = f"https://www.youtube.com/watch?v={video_id}" + + if meta.get("unavailable"): + title = f"_(unavailable — {meta.get('reason', 'unknown')})_" + channel = "—" + uploaded = "—" + duration = "—" + views = "—" + likes = "—" + else: + title = meta.get("title") or curated.get("title") or "" + # Escape pipes in titles so table rendering doesn't break. + title = title.replace("|", "\\|") + channel = (meta.get("uploader") or "—").replace("|", "\\|") + uploaded = fmt_date(meta.get("upload_date", "")) + duration = fmt_duration(meta.get("duration")) + views = fmt_int(meta.get("view_count")) + likes = fmt_int(meta.get("like_count")) + + return f"| `{key}` | {title} | {channel} | {uploaded} | {duration} | {views} | {likes} | [watch]({url}) |" + + +def main() -> int: + with VIDEOS_JSON.open() as f: + data = json.load(f) + + tense_entries = data.get("tenseGuides", {}) + grammar_entries = data.get("grammarNotes", {}) + + # Collect all unique videoIds so we only call yt-dlp once per video + # (several grammar notes reuse tense-guide videos). + video_ids = {e["videoId"] for e in tense_entries.values()} | { + e["videoId"] for e in grammar_entries.values() + } + + print(f"Fetching metadata for {len(video_ids)} unique videos…", file=sys.stderr) + + metadata: dict[str, dict] = {} + with ThreadPoolExecutor(max_workers=8) as pool: + future_to_id = {pool.submit(fetch_metadata, vid): vid for vid in video_ids} + for future in as_completed(future_to_id): + vid = future_to_id[future] + metadata[vid] = future.result() + status = "✗" if metadata[vid].get("unavailable") else "✓" + print(f" {status} {vid}", file=sys.stderr) + + missing_tenses = [tid for tid in EXPECTED_TENSE_IDS if tid not in tense_entries] + missing_grammar = [gid for gid in EXPECTED_GRAMMAR_IDS if gid not in grammar_entries] + + today = date.today().isoformat() + + lines: list[str] = [] + lines.append("# Curated YouTube Videos") + lines.append("") + lines.append( + "Every tense guide and grammar note in the app can be tied to a single " + "curated YouTube video. This file is generated from " + "`Conjuga/youtube_videos.json` by `Scripts/generate_videos_markdown.py` " + "— regenerate when you add or change entries." + ) + lines.append("") + lines.append(f"- Total tense-guide entries: **{len(tense_entries)}** of {len(EXPECTED_TENSE_IDS)}") + lines.append(f"- Total grammar-note entries: **{len(grammar_entries)}** of {len(EXPECTED_GRAMMAR_IDS)}") + lines.append(f"- Last verified: **{today}** (run `python3 Scripts/generate_videos_markdown.py` to refresh)") + lines.append("") + lines.append( + "Like counts are often blank because YouTube hides the public count on " + "most videos for signed-out requests. Titles and durations are pulled " + "live from YouTube; unavailable entries mean the video has been taken " + "down, made private, or region-locked." + ) + lines.append("") + + # Tense guides section + lines.append("## Tense guides") + lines.append("") + lines.append("Tied to `TenseGuide.tenseId` in the Guide tab.") + lines.append("") + lines.append("| Tense ID | Title | Channel | Uploaded | Duration | Views | Likes | URL |") + lines.append("|---|---|---|---|---|---|---|---|") + for tid in EXPECTED_TENSE_IDS: + if tid not in tense_entries: + continue + entry = tense_entries[tid] + lines.append(render_row(tid, entry, metadata.get(entry["videoId"], {}))) + lines.append("") + + # Grammar notes section + lines.append("## Grammar notes") + lines.append("") + lines.append("Tied to `GrammarNote.id` (hand-authored + generated) in the Guide → Grammar tab.") + lines.append("") + lines.append("| Grammar ID | Title | Channel | Uploaded | Duration | Views | Likes | URL |") + lines.append("|---|---|---|---|---|---|---|---|") + for gid in EXPECTED_GRAMMAR_IDS: + if gid not in grammar_entries: + continue + entry = grammar_entries[gid] + lines.append(render_row(gid, entry, metadata.get(entry["videoId"], {}))) + lines.append("") + + # Missing section + if missing_tenses or missing_grammar: + lines.append("## Topics without a curated video") + lines.append("") + lines.append( + "These show a \"No video yet\" label in the app. Add entries to " + "`Conjuga/youtube_videos.json` to fill them in." + ) + lines.append("") + if missing_tenses: + lines.append("**Tense guides:**") + lines.append("") + for tid in missing_tenses: + lines.append(f"- `{tid}`") + lines.append("") + if missing_grammar: + lines.append("**Grammar notes:**") + lines.append("") + for gid in missing_grammar: + lines.append(f"- `{gid}`") + lines.append("") + + OUTPUT_MD.write_text("\n".join(lines)) + print(f"\nWrote {OUTPUT_MD.relative_to(REPO_ROOT)}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main())