diff --git a/k8s/gharaati-crawl-job.yaml b/k8s/gharaati-crawl-job.yaml
new file mode 100644
index 0000000..4e9e5b2
--- /dev/null
+++ b/k8s/gharaati-crawl-job.yaml
@@ -0,0 +1,44 @@
+# One-shot crawl of Tafsir Noor into tafsirquran-audio PVC.
+# Scale API to 0 first (RWO PVC), then create ConfigMap + Job:
+# kubectl -n tafsirquran create configmap tafsirquran-gharaati-scripts \
+# --from-file=crawl-gharaati.py=scripts/crawl-gharaati.py --dry-run=client -o yaml | kubectl apply -f -
+# kubectl -n tafsirquran apply -f k8s/gharaati-crawl-job.yaml
+# kubectl -n tafsirquran logs -f job/tafsirquran-gharaati-crawl
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: tafsirquran-gharaati-crawl
+ namespace: tafsirquran
+spec:
+ backoffLimit: 2
+ ttlSecondsAfterFinished: 86400
+ template:
+ spec:
+ restartPolicy: Never
+ containers:
+ - name: crawl
+ image: python:3.12-slim
+ command: ["/bin/bash", "-lc"]
+ args:
+ - |
+ set -euo pipefail
+ apt-get update -qq
+ 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
+ volumeMounts:
+ - name: audio
+ mountPath: /data/audio
+ - name: scripts
+ mountPath: /scripts
+ resources:
+ requests:
+ memory: 256Mi
+ cpu: 250m
+ volumes:
+ - name: audio
+ persistentVolumeClaim:
+ claimName: tafsirquran-audio
+ - name: scripts
+ configMap:
+ name: tafsirquran-gharaati-scripts
+ defaultMode: 0755
diff --git a/scripts/crawl-gharaati.py b/scripts/crawl-gharaati.py
new file mode 100755
index 0000000..a62f08d
--- /dev/null
+++ b/scripts/crawl-gharaati.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+"""Crawl Tafsir Noor from gharaati.ir into AUDIO_DIR/gharaati + content JSON."""
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import re
+import sys
+import time
+import urllib.error
+import urllib.request
+from html import unescape
+from pathlib import Path
+
+BASE = "https://gharaati.ir"
+UA = "TafsirQuranCrawler/1.0 (+local; educational mirror)"
+
+
+def fetch(url: str, retries: int = 5) -> bytes:
+ last: Exception | None = None
+ for i in range(retries):
+ try:
+ req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "*/*"})
+ with urllib.request.urlopen(req, timeout=60) as resp:
+ return resp.read()
+ except Exception as ex: # noqa: BLE001
+ last = ex
+ time.sleep(1.5 * (i + 1))
+ raise RuntimeError(f"fetch failed {url}: {last}")
+
+
+def strip_tags(html: str) -> str:
+ text = re.sub(r"
", "\n", html, flags=re.I)
+ text = re.sub(r"
صوت این آیه در دسترس نیست
+ }در حال بارگذاری...
} - else if (items.Count == 0) - { -هنوز نشانی ذخیره نشده است.
- } else { -سوره نشانشدهای نیست
+ } + else + { +آیه نشانشدهای نیست
+ } + else + { +در حال بارگذاری...
} else if (items.Count == 0) { -هنوز علاقهمندیای ندارید.
+آیه علاقهمندی ثبت نشده
} else { -@author
+ @if (view == View.Ayahs || view == View.Detail) + { + + }در حال بارگذاری...
} - else + else if (view == View.Surahs) { -