Add curated-videos markdown report + generator script
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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`
|
||||
@@ -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: <reason>. <cookie/help nag with URLs…>"
|
||||
# Extract just <reason> 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())
|
||||
Reference in New Issue
Block a user