Skip re-downloading site MP3s; reuse existing Gooya audio on PVC.
build-and-deploy / deploy (push) Successful in 2m34s
build-and-deploy / deploy (push) Successful in 2m34s
Crawl only ayah text/notes/messages; API falls back to episode audio.
This commit is contained in:
@@ -24,7 +24,8 @@ spec:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ca-certificates >/dev/null
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ca-certificates >/dev/null
|
||||||
python3 /scripts/crawl-gharaati.py /data/audio --skip-existing-json --delay 0.25
|
# Content only — PVC already has ~2997 Gooya MP3s; skip site audio re-download.
|
||||||
|
python3 /scripts/crawl-gharaati.py /data/audio --skip-existing-json --delay 0.15
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: audio
|
- name: audio
|
||||||
mountPath: /data/audio
|
mountPath: /data/audio
|
||||||
|
|||||||
+25
-26
@@ -1,5 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Crawl Tafsir Noor from gharaati.ir into AUDIO_DIR/gharaati + content JSON."""
|
"""Crawl Tafsir Noor text/notes/messages from gharaati.ir (JSON only by default).
|
||||||
|
|
||||||
|
Existing PVC audio (Tafsir Gooya RARs) is kept; do not re-download site MP3s
|
||||||
|
unless --with-audio is passed.
|
||||||
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -8,7 +12,6 @@ import os
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import urllib.error
|
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from html import unescape
|
from html import unescape
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -38,20 +41,15 @@ def strip_tags(html: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def section_after_title(html: str, title: str) -> str | None:
|
def section_after_title(html: str, title: str) -> str | None:
|
||||||
# Match heading text then first content cell with class tafsir or tafsir_ayeh
|
|
||||||
pattern = (
|
pattern = (
|
||||||
rf'<span class="post-title h-text">\s*{re.escape(title)}\s*</span>.*?'
|
rf'<span class="post-title h-text">\s*{re.escape(title)}\s*</span>.*?'
|
||||||
rf'<td[^>]*class=[\'"]?(?:tafsir_ayeh|tafsir)[\'"]?[^>]*>(.*?)</td>'
|
rf'<td[^>]*class=[\'"]?(?:tafsir_ayeh|tafsir)[\'"]?[^>]*>(.*?)</td>'
|
||||||
)
|
)
|
||||||
m = re.search(pattern, html, flags=re.I | re.S)
|
m = re.search(pattern, html, flags=re.I | re.S)
|
||||||
if not m:
|
return m.group(1).strip() if m else None
|
||||||
# alternate spellings
|
|
||||||
return None
|
|
||||||
return m.group(1).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def parse_show3(html: str) -> dict:
|
def parse_show3(html: str) -> dict:
|
||||||
# Normalize common title variants
|
|
||||||
ayah_html = section_after_title(html, "آيه") or section_after_title(html, "آیه")
|
ayah_html = section_after_title(html, "آيه") or section_after_title(html, "آیه")
|
||||||
tarjome = section_after_title(html, "ترجمه")
|
tarjome = section_after_title(html, "ترجمه")
|
||||||
nokteh = section_after_title(html, "نکته ها") or section_after_title(html, "نكته ها")
|
nokteh = section_after_title(html, "نکته ها") or section_after_title(html, "نكته ها")
|
||||||
@@ -98,18 +96,18 @@ def download_mp3(surah: int, ayah: int, dest: Path) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def crawl_one(surah: int, ayah: int, audio_dir: Path, delay: float) -> dict:
|
def crawl_one(surah: int, ayah: int, audio_dir: Path, delay: float, with_audio: bool) -> dict:
|
||||||
html = fetch(f"{BASE}/show3.php?page=tafsir3&numsooreh={surah}&numayeh={ayah}").decode(
|
html = fetch(f"{BASE}/show3.php?page=tafsir3&numsooreh={surah}&numayeh={ayah}").decode(
|
||||||
"utf-8", "replace"
|
"utf-8", "replace"
|
||||||
)
|
)
|
||||||
parsed = parse_show3(html)
|
parsed = parse_show3(html)
|
||||||
rel = f"gharaati/{surah}/{ayah}.mp3"
|
parsed["audioPath"] = None
|
||||||
mp3_path = audio_dir / rel
|
if with_audio:
|
||||||
download_mp3(surah, ayah, mp3_path)
|
rel = f"gharaati/{surah}/{ayah}.mp3"
|
||||||
if mp3_path.exists():
|
mp3_path = audio_dir / rel
|
||||||
parsed["audioPath"] = rel
|
download_mp3(surah, ayah, mp3_path)
|
||||||
else:
|
if mp3_path.exists():
|
||||||
parsed["audioPath"] = None
|
parsed["audioPath"] = rel
|
||||||
parsed["surah"] = surah
|
parsed["surah"] = surah
|
||||||
parsed["ayah"] = ayah
|
parsed["ayah"] = ayah
|
||||||
content_path = audio_dir / "gharaati" / "content" / str(surah) / f"{ayah}.json"
|
content_path = audio_dir / "gharaati" / "content" / str(surah) / f"{ayah}.json"
|
||||||
@@ -124,8 +122,13 @@ def main() -> int:
|
|||||||
ap.add_argument("audio_dir", nargs="?", default=os.environ.get("AUDIO_DIR", "/data/audio"))
|
ap.add_argument("audio_dir", nargs="?", default=os.environ.get("AUDIO_DIR", "/data/audio"))
|
||||||
ap.add_argument("--surah-from", type=int, default=1)
|
ap.add_argument("--surah-from", type=int, default=1)
|
||||||
ap.add_argument("--surah-to", type=int, default=114)
|
ap.add_argument("--surah-to", type=int, default=114)
|
||||||
ap.add_argument("--delay", type=float, default=0.35)
|
ap.add_argument("--delay", type=float, default=0.2)
|
||||||
ap.add_argument("--skip-existing-json", action="store_true")
|
ap.add_argument("--skip-existing-json", action="store_true")
|
||||||
|
ap.add_argument(
|
||||||
|
"--with-audio",
|
||||||
|
action="store_true",
|
||||||
|
help="Also download archive/tafsir/{s}/{a}.mp3 (usually unnecessary; PVC already has Gooya audio)",
|
||||||
|
)
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
audio_dir = Path(args.audio_dir)
|
audio_dir = Path(args.audio_dir)
|
||||||
audio_dir.mkdir(parents=True, exist_ok=True)
|
audio_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -136,24 +139,20 @@ def main() -> int:
|
|||||||
if not ayahs:
|
if not ayahs:
|
||||||
print(f"surah {s}: no ayah list", file=sys.stderr)
|
print(f"surah {s}: no ayah list", file=sys.stderr)
|
||||||
continue
|
continue
|
||||||
print(f"surah {s}: {len(ayahs)} ayahs")
|
print(f"surah {s}: {len(ayahs)} ayahs", flush=True)
|
||||||
for a in ayahs:
|
for a in ayahs:
|
||||||
jpath = audio_dir / "gharaati" / "content" / str(s) / f"{a}.json"
|
jpath = audio_dir / "gharaati" / "content" / str(s) / f"{a}.json"
|
||||||
if args.skip_existing_json and jpath.exists():
|
if args.skip_existing_json and jpath.exists():
|
||||||
# still try mp3 if missing
|
|
||||||
mp3 = audio_dir / "gharaati" / str(s) / f"{a}.mp3"
|
|
||||||
if not mp3.exists():
|
|
||||||
download_mp3(s, a, mp3)
|
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
crawl_one(s, a, audio_dir, args.delay)
|
crawl_one(s, a, audio_dir, args.delay, args.with_audio)
|
||||||
total += 1
|
total += 1
|
||||||
if total % 25 == 0:
|
if total % 50 == 0:
|
||||||
print(f" crawled {total} ayahs (at {s}:{a})")
|
print(f" crawled {total} ayahs (at {s}:{a})", flush=True)
|
||||||
except Exception as ex: # noqa: BLE001
|
except Exception as ex: # noqa: BLE001
|
||||||
print(f"FAIL {s}:{a} {ex}", file=sys.stderr)
|
print(f"FAIL {s}:{a} {ex}", file=sys.stderr)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
print(f"done. crawled={total}")
|
print(f"done. crawled={total}", flush=True)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -97,18 +97,26 @@ public static class TafsirEndpoints
|
|||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var episodeAyahs = (await db.Episodes.AsNoTracking()
|
||||||
|
.Where(e => e.TafsirId == tafsir.Id && e.SurahId == surah.Id)
|
||||||
|
.Select(e => e.AyahNumber)
|
||||||
|
.Distinct()
|
||||||
|
.ToListAsync(ct)).ToHashSet();
|
||||||
|
|
||||||
var list = ayahs.Select(a =>
|
var list = ayahs.Select(a =>
|
||||||
{
|
{
|
||||||
contents.TryGetValue(a.Number, out var c);
|
contents.TryGetValue(a.Number, out var c);
|
||||||
var preview = a.TextFa;
|
var preview = a.TextFa;
|
||||||
if (!string.IsNullOrWhiteSpace(preview) && preview.Length > 80)
|
if (!string.IsNullOrWhiteSpace(preview) && preview.Length > 80)
|
||||||
preview = preview[..80] + "…";
|
preview = preview[..80] + "…";
|
||||||
|
var hasAudio = !string.IsNullOrWhiteSpace(c?.AudioPath) || episodeAyahs.Contains(a.Number)
|
||||||
|
|| episodeAyahs.Count > 0;
|
||||||
return new AyahListItemDto(
|
return new AyahListItemDto(
|
||||||
surah.Number,
|
surah.Number,
|
||||||
surah.NameFa,
|
surah.NameFa,
|
||||||
a.Number,
|
a.Number,
|
||||||
preview,
|
preview,
|
||||||
!string.IsNullOrWhiteSpace(c?.AudioPath),
|
hasAudio,
|
||||||
bookmarks.Contains(a.Number),
|
bookmarks.Contains(a.Number),
|
||||||
favorites.Contains(a.Number));
|
favorites.Contains(a.Number));
|
||||||
}).ToList();
|
}).ToList();
|
||||||
@@ -156,7 +164,24 @@ public static class TafsirEndpoints
|
|||||||
|
|
||||||
string? audioUrl = null;
|
string? audioUrl = null;
|
||||||
if (!string.IsNullOrWhiteSpace(content?.AudioPath))
|
if (!string.IsNullOrWhiteSpace(content?.AudioPath))
|
||||||
|
{
|
||||||
audioUrl = $"/api/audio/ayah/{surahNumber}/{ayahNumber}?tafsirId={tafsir.Id}";
|
audioUrl = $"/api/audio/ayah/{surahNumber}/{ayahNumber}?tafsirId={tafsir.Id}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fall back to existing Gooya episode audio already on PVC (best-effort ayah match).
|
||||||
|
var episode = await db.Episodes.AsNoTracking()
|
||||||
|
.Where(e => e.TafsirId == tafsir.Id && e.SurahId == surah.Id && e.AyahNumber == ayahNumber)
|
||||||
|
.OrderBy(e => e.SortOrder)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
episode ??= await db.Episodes.AsNoTracking()
|
||||||
|
.Where(e => e.TafsirId == tafsir.Id && e.SurahId == surah.Id)
|
||||||
|
.OrderBy(e => Math.Abs(e.AyahNumber - ayahNumber))
|
||||||
|
.ThenBy(e => e.SortOrder)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
if (episode is not null)
|
||||||
|
audioUrl = $"/api/audio/{episode.Id}";
|
||||||
|
}
|
||||||
|
|
||||||
return Results.Ok(new AyahDetailDto(
|
return Results.Ok(new AyahDetailDto(
|
||||||
tafsir.Id,
|
tafsir.Id,
|
||||||
|
|||||||
Reference in New Issue
Block a user