feat(quantic): curs NLP Quantic (curs/34) - audio pentru telefon

- courses.py: curs "quantic" (shop.cursnlp.ro/curs/34, root nlp-quantic/)
- download.py: yt-dlp cu 8 fragmente paralele + fara progress bar
- rename_phone_quantic.py: copii MP3 cu ID3 (titlu, album per modul, track) + coperta
- rename_phone_practitioner.py: font fallback Linux, an pe coperta din YEAR
- nlp-quantic/: coperti M1-M5, pagina index.html pentru descarcare pe telefon, PDF-uri rezumat

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017NC1T6XwMepWVPuGZELZtT
This commit is contained in:
Claude Agent
2026-09-19 17:19:29 +00:00
parent 7ee5c94617
commit e8b44473af
13 changed files with 134 additions and 1 deletions

View File

@@ -28,6 +28,15 @@ COURSES = {
"env_pass": "PRACTITIONER_PASSWORD", "env_pass": "PRACTITIONER_PASSWORD",
"root": Path("nlp-practitioner"), "root": Path("nlp-practitioner"),
}, },
"quantic": {
"name": "NLP Quantic (cursnlp.ro)",
"base_url": "https://shop.cursnlp.ro",
"course_path": "/curs/34",
"login_path": "/login",
"env_user": "PRACTITIONER_USERNAME",
"env_pass": "PRACTITIONER_PASSWORD",
"root": Path("nlp-quantic"),
},
} }

View File

@@ -278,6 +278,8 @@ def download_vimeo_audio(vimeo_url: str, referer: str, dest: Path) -> bool:
"http_headers": {"Referer": referer}, "http_headers": {"Referer": referer},
"quiet": True, "quiet": True,
"no_warnings": True, "no_warnings": True,
"noprogress": True,
"concurrent_fragment_downloads": 8,
"postprocessors": [{ "postprocessors": [{
"key": "FFmpegExtractAudio", "key": "FFmpegExtractAudio",
"preferredcodec": "mp3", "preferredcodec": "mp3",

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,58 @@
<!doctype html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NLP Quantic audio</title>
<style>
:root { --bg:#fff; --fg:#1a1a1a; --muted:#666; --line:#e3e3e3; --btn:#8e2a22; --btnfg:#fff; }
@media (prefers-color-scheme: dark) { :root { --bg:#121212; --fg:#eee; --muted:#aaa; --line:#333; --btn:#c0504a; } }
body { margin:0; padding:16px; background:var(--bg); color:var(--fg); font:16px/1.4 system-ui, sans-serif; max-width:720px; }
h1 { font-size:1.4em; margin:0 0 12px; }
h2 { font-size:1.1em; margin:24px 0 8px; display:flex; justify-content:space-between; align-items:center; gap:8px; }
button { background:var(--btn); color:var(--btnfg); border:0; border-radius:8px; padding:10px 14px; font-size:15px; }
button.big { width:100%; padding:14px; font-size:17px; }
.track { border-top:1px solid var(--line); padding:10px 0; }
.track a { color:var(--fg); }
audio { width:100%; margin-top:6px; }
#status { color:var(--muted); margin-top:8px; min-height:1.4em; }
</style>
</head>
<body>
<h1>NLP Quantic – audio</h1>
<button class="big" onclick="downloadAll(document.querySelectorAll('a.mp3'))">Descarcă tot (13 fișiere, 1.8 GB)</button>
<div id="status">Dacă browserul întreabă „permiți descărcări multiple?”, apasă Permite.</div>
<div id="list"></div>
<script>
const MODULES = {
"Quantic M1": ["Q1-01 NLP Quantic M1Z1.mp3", "Q1-02 NLP Quantic M1Z2.mp3", "Q1-03 Workshop Bonus - Experiente in apropierea mortii.mp3"],
"Quantic M2": ["Q2-01 Modulul 2 - ziua 1.mp3", "Q2-02 Modulul 2 - ziua 2.mp3"],
"Quantic M3": ["Q3-01 Modulul 3 ziua 1.mp3", "Q3-02 Modulul 3 ziua 2.mp3"],
"Quantic M4": ["Q4-01 Modulul 4 ziua 1.mp3", "Q4-02 Modulul 4 ziua 2.mp3"],
"Quantic M5": ["Q5-01 Modulul 5 ziua 1.mp3", "Q5-02 Modulul 5 ziua 2.mp3", "Q5-03 Workshop Bonus.mp3", "Q5-04 Workshop Bonus - Karma si Reincarnarea.mp3"],
};
const list = document.getElementById("list");
for (const [dir, files] of Object.entries(MODULES)) {
const sec = document.createElement("section");
sec.innerHTML = `<h2>${dir}<button>Descarcă modulul</button></h2>`;
for (const f of files) {
const url = encodeURIComponent(dir) + "/" + encodeURIComponent(f);
sec.insertAdjacentHTML("beforeend",
`<div class="track"><a class="mp3" href="${url}" download="${f}">${f.replace(".mp3", "")}</a>
<audio controls preload="none" src="${url}"></audio></div>`);
}
sec.querySelector("h2 button").onclick = () => downloadAll(sec.querySelectorAll("a.mp3"));
list.appendChild(sec);
}
async function downloadAll(links) {
const status = document.getElementById("status");
for (let i = 0; i < links.length; i++) {
status.textContent = `Pornesc descărcarea ${i + 1}/${links.length}: ${links[i].textContent}`;
links[i].click();
await new Promise(r => setTimeout(r, 2500)); // browsers drop rapid-fire download clicks
}
status.textContent = `Gata: ${links.length} descărcări pornite. Le găsești în folderul Download.`;
}
</script>
</body>
</html>

Binary file not shown.

View File

@@ -49,6 +49,7 @@ def find_font(size: int) -> ImageFont.FreeTypeFont:
"C:/Windows/Fonts/arialbd.ttf", "C:/Windows/Fonts/arialbd.ttf",
"C:/Windows/Fonts/arial.ttf", "C:/Windows/Fonts/arial.ttf",
"C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/segoeui.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
] ]
for c in candidates: for c in candidates:
if Path(c).exists(): if Path(c).exists():
@@ -109,7 +110,7 @@ def make_cover(module_num: int, theme_full: str, out_path: Path) -> Path:
# Course label (small, top) # Course label (small, top)
f_small = find_font(36) f_small = find_font(36)
label = COURSE_PREFIX + " - 2024" label = f"{COURSE_PREFIX} - {YEAR}"
bbox = draw.textbbox((0, 0), label, font=f_small) bbox = draw.textbbox((0, 0), label, font=f_small)
draw.text(((size - (bbox[2] - bbox[0])) / 2, 60), label, font=f_small, fill="#ffffff") draw.text(((size - (bbox[2] - bbox[0])) / 2, 60), label, font=f_small, fill="#ffffff")

63
rename_phone_quantic.py Normal file
View File

@@ -0,0 +1,63 @@
"""Copies NLP Quantic MP3s to nlp-quantic/audio_phone/ with phone-friendly names + ID3 tags + cover.
Reuses cover/tag helpers from rename_phone_practitioner.py.
Run after: python download.py --course quantic
"""
from __future__ import annotations
import json
import re
import shutil
import sys
from pathlib import Path
import rename_phone_practitioner as rp
ROOT = Path(__file__).parent
QUANTIC = ROOT / "nlp-quantic"
AUDIO_DST = QUANTIC / "audio_phone"
COVERS_DIR = AUDIO_DST / "_covers"
MANIFEST = QUANTIC / "manifest.json"
rp.COURSE_PREFIX = "NLP Quantic"
rp.ARTIST = "cursnlp.ro"
rp.ALBUM_ARTIST = "cursnlp.ro"
rp.YEAR = "2023"
def clean_title(t: str) -> str:
return re.sub(r"\s+", " ", t.replace(",", " ")).strip()
def main() -> int:
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
errors = 0
for mod in manifest["modules"]:
n = int(mod["name"].split()[-1])
lecs = [L for L in mod["lectures"] if L["type"] in ("audio", "vimeo")]
if not lecs:
continue
cover = COVERS_DIR / f"M{n}.png"
if not cover.exists():
rp.make_cover(n, "", cover)
album = f"NLP Quantic - Modul {n}"
out_dir = AUDIO_DST / f"Quantic M{n}"
out_dir.mkdir(parents=True, exist_ok=True)
for idx, lec in enumerate(lecs, 1):
src = ROOT / lec["audio_path"]
if lec.get("download_status") != "complete" or not src.exists():
print(f" MISSING: {lec['title']}")
errors += 1
continue
title = clean_title(lec["title"])
dst = out_dir / f"Q{n}-{idx:02d} {rp.safe_filename_part(title)}.mp3"
if not (dst.exists() and dst.stat().st_size >= src.stat().st_size):
shutil.copy2(src, dst)
rp.tag_mp3(dst, title=title, album=album, track_num=idx, track_total=len(lecs), cover_png=cover)
print(f" ok: {dst.relative_to(QUANTIC)}")
print(f"\nDONE. errors={errors} Output: {AUDIO_DST}")
return 0 if errors == 0 else 2
if __name__ == "__main__":
sys.exit(main())