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:
@@ -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"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
BIN
nlp-quantic/audio_phone/_covers/M1.png
Normal file
BIN
nlp-quantic/audio_phone/_covers/M1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
nlp-quantic/audio_phone/_covers/M2.png
Normal file
BIN
nlp-quantic/audio_phone/_covers/M2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
nlp-quantic/audio_phone/_covers/M3.png
Normal file
BIN
nlp-quantic/audio_phone/_covers/M3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
nlp-quantic/audio_phone/_covers/M4.png
Normal file
BIN
nlp-quantic/audio_phone/_covers/M4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
nlp-quantic/audio_phone/_covers/M5.png
Normal file
BIN
nlp-quantic/audio_phone/_covers/M5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
58
nlp-quantic/audio_phone/index.html
Normal file
58
nlp-quantic/audio_phone/index.html
Normal 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>
|
||||||
BIN
nlp-quantic/summaries/pdf/NLP Quantic M3 _ 16sept2023.pdf
Normal file
BIN
nlp-quantic/summaries/pdf/NLP Quantic M3 _ 16sept2023.pdf
Normal file
Binary file not shown.
BIN
nlp-quantic/summaries/pdf/NLP Quantic Modulul 2.pdf
Normal file
BIN
nlp-quantic/summaries/pdf/NLP Quantic Modulul 2.pdf
Normal file
Binary file not shown.
Binary file not shown.
@@ -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
63
rename_phone_quantic.py
Normal 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())
|
||||||
Reference in New Issue
Block a user