Migrate to Blazor WASM, MSSQL platform DB, and durable audio PVC.
build-and-deploy / deploy (push) Failing after 15m11s
build-and-deploy / deploy (push) Failing after 15m11s
Remove in-cluster Postgres; serve WASM via nginx; add audio import job for Gooya archives.
This commit is contained in:
+7
-7
@@ -5,6 +5,7 @@ ARG http_proxy
|
||||
ARG https_proxy
|
||||
ARG NO_PROXY
|
||||
ARG no_proxy
|
||||
ARG API_BASE=http://185.252.31.44:30821/
|
||||
WORKDIR /src
|
||||
COPY NuGet.Config ./
|
||||
COPY src/TafsirQuran.Shared/TafsirQuran.Shared.csproj src/TafsirQuran.Shared/
|
||||
@@ -14,11 +15,10 @@ RUN dotnet restore src/TafsirQuran.Web/TafsirQuran.Web.csproj
|
||||
COPY src/TafsirQuran.Shared/ src/TafsirQuran.Shared/
|
||||
COPY src/TafsirQuran.UI/ src/TafsirQuran.UI/
|
||||
COPY src/TafsirQuran.Web/ src/TafsirQuran.Web/
|
||||
RUN dotnet publish src/TafsirQuran.Web/TafsirQuran.Web.csproj -c Release -o /app/publish --no-restore
|
||||
RUN printf '{\n "ApiBaseAddress": "%s"\n}\n' "$API_BASE" > src/TafsirQuran.Web/wwwroot/appsettings.json \
|
||||
&& dotnet publish src/TafsirQuran.Web/TafsirQuran.Web.csproj -c Release -o /app/publish
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
|
||||
WORKDIR /app
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
EXPOSE 8080
|
||||
COPY --from=build /app/publish .
|
||||
ENTRYPOINT ["dotnet", "TafsirQuran.Web.dll"]
|
||||
FROM nginx:1.27-alpine AS final
|
||||
COPY docker/nginx-web.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/publish/wwwroot /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
|
||||
@@ -7,10 +7,18 @@
|
||||
- ریپو: https://git.se.kbs1.ir/admin/TafsirQuran
|
||||
- CI: `.gitea/workflows/deploy.yaml` روی runner label `deploy`
|
||||
- Namespace: `tafsirquran`
|
||||
- وب: http://185.252.31.44:30820/
|
||||
- وب (Blazor WASM): http://185.252.31.44:30820/
|
||||
- API: http://185.252.31.44:30821/
|
||||
- DB: MSSQL `mssql-svc.platform` / database `TafsirQuran`
|
||||
- صوت: PVC `tafsirquran-audio` → mount `/data/audio`
|
||||
|
||||
پوش به `main` یا `workflow_dispatch` تصویرها را میسازد، به k3s import میکند و رولاوت میکند.
|
||||
پوش به `main` دیپلوی میکند. ایمپورت صوت (یکبار، طولانی):
|
||||
|
||||
```bash
|
||||
kubectl -n tafsirquran apply -f k8s/audio-import-job.yaml
|
||||
kubectl -n tafsirquran logs -f job/tafsirquran-audio-import
|
||||
kubectl -n tafsirquran rollout restart deploy/tafsirquran-api # resync episodes
|
||||
```
|
||||
|
||||
## پیشنیاز توسعه محلی
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(dll|wasm|dat|blat|json|js|css|png|svg|woff2?)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
try_files $uri =404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
# One-shot import of Tafsir Gooya audio into tafsirquran-audio PVC.
|
||||
# Run: kubectl -n tafsirquran apply -f k8s/audio-import-job.yaml
|
||||
# Watch: kubectl -n tafsirquran logs -f job/tafsirquran-audio-import
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: tafsirquran-audio-import
|
||||
namespace: tafsirquran
|
||||
spec:
|
||||
backoffLimit: 1
|
||||
ttlSecondsAfterFinished: 86400
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: import
|
||||
image: debian:bookworm-slim
|
||||
command: ["/bin/bash", "-lc"]
|
||||
args:
|
||||
- |
|
||||
set -euo pipefail
|
||||
apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq curl unrar ca-certificates >/dev/null
|
||||
chmod +x /scripts/fetch-audio.sh
|
||||
/scripts/fetch-audio.sh /data/audio
|
||||
volumeMounts:
|
||||
- name: audio
|
||||
mountPath: /data/audio
|
||||
- name: scripts
|
||||
mountPath: /scripts
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 250m
|
||||
volumes:
|
||||
- name: audio
|
||||
persistentVolumeClaim:
|
||||
claimName: tafsirquran-audio
|
||||
- name: scripts
|
||||
configMap:
|
||||
name: tafsirquran-audio-scripts
|
||||
defaultMode: 0755
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: tafsirquran-audio-scripts
|
||||
namespace: tafsirquran
|
||||
data:
|
||||
fetch-audio.sh: |
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
AUDIO_DIR="${1:-/data/audio}"
|
||||
PASSWORD="www.p30download.com"
|
||||
BASE_URL="https://pdn.sharezilla.ir/d/audio"
|
||||
WORKDIR="${AUDIO_DIR}/.download"
|
||||
mkdir -p "$AUDIO_DIR" "$WORKDIR"
|
||||
SLUGS=(
|
||||
001.Tafsir.Gooya.Hamd 002.Tafsir.Gooya.Baqara 003.Tafsir.Gooya.Ale.imran 004.Tafsir.Gooya.Nisa
|
||||
005.Tafsir.Gooya.Maeda 006.Tafsir.Gooya.Anaam 007.Tafsir.Gooya.Araf 008.Tafsir.Gooya.Anfal
|
||||
009.Tafsir.Gooya.Tawba 010.Tafsir.Gooya.Yunus 011.Tafsir.Gooya.Hud 012.Tafsir.Gooya.Yusuf
|
||||
013.Tafsir.Gooya.Rad 014.Tafsir.Gooya.Ibrahim 015.Tafsir.Gooya.Hijr 016.Tafsir.Gooya.Nahl
|
||||
017.Tafsir.Gooya.Isra 018.Tafsir.Gooya.Kahf 019.Tafsir.Gooya.Maryam 020.Tafsir.Gooya.Taha
|
||||
021.Tafsir.Gooya.Anbiya 022.Tafsir.Gooya.Hajj 023.Tafsir.Gooya.Mumenoon 024.Tafsir.Gooya.Noor
|
||||
025.Tafsir.Gooya.Furqan 026.Tafsir.Gooya.Shuara 027.Tafsir.Gooya.Naml 028.Tafsir.Gooya.Qasas
|
||||
029.Tafsir.Gooya.Ankaboot 030.Tafsir.Gooya.Room 031.Tafsir.Gooya.Luqman 032.Tafsir.Gooya.Sajda
|
||||
033.Tafsir.Gooya.Ahzab 034.Tafsir.Gooya.Saba 035.Tafsir.Gooya.Fatir 036.Tafsir.Gooya.Yaseen
|
||||
037.Tafsir.Gooya.Saaffat 038.Tafsir.Gooya.Sad 039.Tafsir.Gooya.Zumar 040.Tafsir.Gooya.Ghafir
|
||||
041.Tafsir.Gooya.Fussilat 042.Tafsir.Gooya.Shura 043.Tafsir.Gooya.Zukhruf 044.Tafsir.Gooya.Dukhan
|
||||
045.Tafsir.Gooya.Jathiya 046.Tafsir.Gooya.Ahqaf 047.Tafsir.Gooya.Muhammad 048.Tafsir.Gooya.Fath
|
||||
049.Tafsir.Gooya.Hujaraat 050.Tafsir.Gooya.Qaf 051.Tafsir.Gooya.Dhariyat 052.Tafsir.Gooya.Tur
|
||||
053.Tafsir.Gooya.Najm 054.Tafsir.Gooya.Qamar 055.Tafsir.Gooya.Ar-Rahman 056.Tafsir.Gooya.Waqia
|
||||
057.Tafsir.Gooya.Hadid 058.Tafsir.Gooya.Mujadila 059.Tafsir.Gooya.Hashr 060.Tafsir.Gooya.Mumtahina
|
||||
061.Tafsir.Gooya.Saff 062.Tafsir.Gooya.Jumua 063.Tafsir.Gooya.Munafiqoon 064.Tafsir.Gooya.Taghabun
|
||||
065.Tafsir.Gooya.Talaq 066.Tafsir.Gooya.Tahrim 067.Tafsir.Gooya.Mulk 068.Tafsir.Gooya.Qalam
|
||||
069.Tafsir.Gooya.Haaqqa 070.Tafsir.Gooya.Maarij 071.Tafsir.Gooya.Nooh 072.Tafsir.Gooya.Jinn
|
||||
073.Tafsir.Gooya.Muzzammil 074.Tafsir.Gooya.muddaththir 075.Tafsir.Gooya.Qiyama 076.Tafsir.Gooya.Insan
|
||||
077.Tafsir.Gooya.Mursalat 078.Tafsir.Gooya.Naba 079.Tafsir.Gooya.Naziat 080.Tafsir.Gooya.Abasa
|
||||
081.Tafsir.Gooya.Takwir 082.Tafsir.Gooya.Infitar 083.Tafsir.Gooya.Mutaffifin 084.Tafsir.Gooya.Inshiqaq
|
||||
085.Tafsir.Gooya.Brooj 086.Tafsir.Gooya.Tariq 087.Tafsir.Gooya.Ala 088.Tafsir.Gooya.Ghashiya
|
||||
089.Tafsir.Gooya.Fajr 090.Tafsir.Gooya.Balad 091.Tafsir.Gooya.Shams 092.Tafsir.Gooya.Lail
|
||||
093.Tafsir.Gooya.Dhuha 094.Tafsir.Gooya.Inshirah 095.Tafsir.Gooya.Tin 096.Tafsir.Gooya.Alaq
|
||||
097.Tafsir.Gooya.Qadr 098.Tafsir.Gooya.Bayyina 099.Tafsir.Gooya.Zalzala 100.Tafsir.Gooya.Adiyat
|
||||
101.Tafsir.Gooya.Qaria 102.Tafsir.Gooya.Takathur 103.Tafsir.Gooya.Asr 104.Tafsir.Gooya.Humaza
|
||||
105.Tafsir.Gooya.Fil 106.Tafsir.Gooya.Quarish 107.Tafsir.Gooya.Maun 108.Tafsir.Gooya.kauther
|
||||
109.Tafsir.Gooya.Kafiroon 110.Tafsir.Gooya.Nasr 111.Tafsir.Gooya.Masad 112.Tafsir.Gooya.Ikhlas
|
||||
113.Tafsir.Gooya.Falaq 114.Tafsir.Gooya.Nas
|
||||
)
|
||||
for slug in "${SLUGS[@]}"; do
|
||||
num="${slug%%.*}"
|
||||
out_dir="${AUDIO_DIR}/${num}"
|
||||
if find "$out_dir" -type f \( -iname '*.mp3' -o -iname '*.m4a' \) 2>/dev/null | grep -q .; then
|
||||
echo "skip $num"; continue
|
||||
fi
|
||||
rar="${WORKDIR}/${slug}.rar"
|
||||
echo "download $slug"
|
||||
curl -fL --retry 5 --retry-delay 3 -o "$rar" "${BASE_URL}/${slug}.rar"
|
||||
mkdir -p "$out_dir"
|
||||
echo "extract $slug"
|
||||
unrar x -o+ -p"${PASSWORD}" "$rar" "${out_dir}/"
|
||||
rm -f "$rar"
|
||||
echo "done $num"
|
||||
done
|
||||
echo "files:" $(find "$AUDIO_DIR" -type f \( -iname '*.mp3' -o -iname '*.m4a' \) | wc -l)
|
||||
+26
-72
@@ -10,8 +10,9 @@ metadata:
|
||||
namespace: tafsirquran
|
||||
type: Opaque
|
||||
stringData:
|
||||
POSTGRES_PASSWORD: tafsir_pg_2026
|
||||
MSSQL_SA_PASSWORD: "87zH26nbqT"
|
||||
Jwt__Key: TafsirQuran-Prod-Secret-Key-Change-Me-32chars
|
||||
ConnectionStrings__Default: "Server=mssql-svc.platform.svc.cluster.local,1433;Database=TafsirQuran;User Id=sa;Password=87zH26nbqT;TrustServerCertificate=True;Encrypt=False"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
@@ -20,7 +21,7 @@ metadata:
|
||||
namespace: tafsirquran
|
||||
data:
|
||||
ASPNETCORE_URLS: "http://+:5080"
|
||||
ConnectionStrings__Default: "Host=tafsirquran-postgres;Port=5432;Database=tafsirquran;Username=tafsir;Password=tafsir_pg_2026"
|
||||
AudioRoot: "/data/audio"
|
||||
Jwt__Issuer: "TafsirQuran"
|
||||
Jwt__Audience: "TafsirQuran"
|
||||
Jwt__ExpireMinutes: "43200"
|
||||
@@ -31,75 +32,19 @@ metadata:
|
||||
name: tafsirquran-web-env
|
||||
namespace: tafsirquran
|
||||
data:
|
||||
ASPNETCORE_URLS: "http://+:8080"
|
||||
ApiBaseAddress: "http://185.252.31.44:30821/"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: tafsirquran-pg-data
|
||||
name: tafsirquran-audio
|
||||
namespace: tafsirquran
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: tafsirquran-postgres
|
||||
namespace: tafsirquran
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: tafsirquran-postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: tafsirquran-postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: tafsir
|
||||
- name: POSTGRES_DB
|
||||
value: tafsirquran
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tafsirquran-secrets
|
||||
key: POSTGRES_PASSWORD
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "tafsir", "-d", "tafsirquran"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: tafsirquran-pg-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: tafsirquran-postgres
|
||||
namespace: tafsirquran
|
||||
spec:
|
||||
selector:
|
||||
app: tafsirquran-postgres
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
storage: 100Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -131,18 +76,30 @@ spec:
|
||||
secretKeyRef:
|
||||
name: tafsirquran-secrets
|
||||
key: Jwt__Key
|
||||
- name: ConnectionStrings__Default
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tafsirquran-secrets
|
||||
key: ConnectionStrings__Default
|
||||
volumeMounts:
|
||||
- name: audio
|
||||
mountPath: /data/audio
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 5080
|
||||
initialDelaySeconds: 10
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 5080
|
||||
initialDelaySeconds: 30
|
||||
initialDelaySeconds: 40
|
||||
periodSeconds: 20
|
||||
volumes:
|
||||
- name: audio
|
||||
persistentVolumeClaim:
|
||||
claimName: tafsirquran-audio
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@@ -178,21 +135,18 @@ spec:
|
||||
image: tafsirquran-web:latest
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: tafsirquran-web-env
|
||||
- containerPort: 80
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
port: 80
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
---
|
||||
apiVersion: v1
|
||||
@@ -205,6 +159,6 @@ spec:
|
||||
selector:
|
||||
app: tafsirquran-web
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
nodePort: 30820
|
||||
|
||||
+12
-15
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build images, import into k3s, apply manifests, roll out.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
@@ -30,9 +29,10 @@ fi
|
||||
build_with_retry() {
|
||||
local dockerfile="$1"
|
||||
local tag="$2"
|
||||
shift 2
|
||||
local attempt=1
|
||||
local max_attempts=3
|
||||
until docker build "${BUILD_ARGS[@]}" -f "${dockerfile}" -t "${tag}" .; do
|
||||
until docker build "${BUILD_ARGS[@]}" "$@" -f "${dockerfile}" -t "${tag}" .; do
|
||||
if [ "$attempt" -ge "$max_attempts" ]; then
|
||||
echo "docker build ${tag} failed after ${max_attempts} attempts" >&2
|
||||
exit 1
|
||||
@@ -43,9 +43,14 @@ build_with_retry() {
|
||||
done
|
||||
}
|
||||
|
||||
echo "==> build api + web"
|
||||
echo "==> remove legacy postgres resources if present"
|
||||
kubectl -n "${NS}" delete deploy tafsirquran-postgres --ignore-not-found
|
||||
kubectl -n "${NS}" delete svc tafsirquran-postgres --ignore-not-found
|
||||
kubectl -n "${NS}" delete pvc tafsirquran-pg-data --ignore-not-found
|
||||
|
||||
echo "==> build api + web (wasm/nginx)"
|
||||
build_with_retry Dockerfile.api "${API_IMAGE}"
|
||||
build_with_retry Dockerfile.web "${WEB_IMAGE}"
|
||||
build_with_retry Dockerfile.web "${WEB_IMAGE}" --build-arg "API_BASE=${PUBLIC_API}/"
|
||||
|
||||
echo "==> import images into k3s"
|
||||
docker save "${API_IMAGE}" | k3s ctr images import -
|
||||
@@ -54,21 +59,12 @@ docker save "${WEB_IMAGE}" | k3s ctr images import -
|
||||
echo "==> apply manifests"
|
||||
kubectl apply -f "${MANIFEST}"
|
||||
|
||||
# Keep public API URL for the Blazor host in sync with NodePort
|
||||
kubectl -n "${NS}" create configmap tafsirquran-web-env \
|
||||
--from-literal=ASPNETCORE_URLS='http://+:8080' \
|
||||
--from-literal=ApiBaseAddress="${PUBLIC_API}/" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "==> wait postgres"
|
||||
kubectl -n "${NS}" rollout status deploy/tafsirquran-postgres --timeout=180s
|
||||
|
||||
echo "==> restart api + web"
|
||||
kubectl -n "${NS}" rollout restart deploy/tafsirquran-api deploy/tafsirquran-web
|
||||
kubectl -n "${NS}" rollout status deploy/tafsirquran-api --timeout=300s
|
||||
kubectl -n "${NS}" rollout status deploy/tafsirquran-api --timeout=360s
|
||||
kubectl -n "${NS}" rollout status deploy/tafsirquran-web --timeout=240s
|
||||
|
||||
kubectl -n "${NS}" get pods,svc -o wide
|
||||
kubectl -n "${NS}" get pods,svc,pvc -o wide
|
||||
|
||||
echo "==> smoke"
|
||||
curl -fsS --max-time 30 "http://127.0.0.1:30821/" | head -c 200; echo
|
||||
@@ -77,4 +73,5 @@ curl -fsS --max-time 30 -o /dev/null -w "web:%{http_code}\n" "http://127.0.0.1:3
|
||||
|
||||
echo "web → ${PUBLIC_WEB}/"
|
||||
echo "api → ${PUBLIC_API}/"
|
||||
echo "audio PVC: tafsirquran-audio (import via k8s/audio-import-job.yaml)"
|
||||
echo "deploy ok"
|
||||
|
||||
Executable
+171
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download Tafsir Gooya (قطره چکانی) RAR archives, extract with password, delete RARs.
|
||||
# Usage: AUDIO_DIR=/data/audio ./scripts/fetch-audio.sh
|
||||
# or: ./scripts/fetch-audio.sh /path/to/audio
|
||||
set -euo pipefail
|
||||
|
||||
AUDIO_DIR="${1:-${AUDIO_DIR:-/data/audio}}"
|
||||
PASSWORD="${RAR_PASSWORD:-www.p30download.com}"
|
||||
BASE_URL="https://pdn.sharezilla.ir/d/audio"
|
||||
WORKDIR="${AUDIO_DIR}/.download"
|
||||
mkdir -p "$AUDIO_DIR" "$WORKDIR"
|
||||
|
||||
need() { command -v "$1" >/dev/null 2>&1 || { echo "missing dependency: $1" >&2; exit 1; }; }
|
||||
need curl
|
||||
if ! command -v unrar >/dev/null 2>&1 && ! command -v 7z >/dev/null 2>&1; then
|
||||
echo "need unrar or 7z" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# slug list matching sharezilla filenames
|
||||
declare -a SLUGS=(
|
||||
"001.Tafsir.Gooya.Hamd"
|
||||
"002.Tafsir.Gooya.Baqara"
|
||||
"003.Tafsir.Gooya.Ale.imran"
|
||||
"004.Tafsir.Gooya.Nisa"
|
||||
"005.Tafsir.Gooya.Maeda"
|
||||
"006.Tafsir.Gooya.Anaam"
|
||||
"007.Tafsir.Gooya.Araf"
|
||||
"008.Tafsir.Gooya.Anfal"
|
||||
"009.Tafsir.Gooya.Tawba"
|
||||
"010.Tafsir.Gooya.Yunus"
|
||||
"011.Tafsir.Gooya.Hud"
|
||||
"012.Tafsir.Gooya.Yusuf"
|
||||
"013.Tafsir.Gooya.Rad"
|
||||
"014.Tafsir.Gooya.Ibrahim"
|
||||
"015.Tafsir.Gooya.Hijr"
|
||||
"016.Tafsir.Gooya.Nahl"
|
||||
"017.Tafsir.Gooya.Isra"
|
||||
"018.Tafsir.Gooya.Kahf"
|
||||
"019.Tafsir.Gooya.Maryam"
|
||||
"020.Tafsir.Gooya.Taha"
|
||||
"021.Tafsir.Gooya.Anbiya"
|
||||
"022.Tafsir.Gooya.Hajj"
|
||||
"023.Tafsir.Gooya.Mumenoon"
|
||||
"024.Tafsir.Gooya.Noor"
|
||||
"025.Tafsir.Gooya.Furqan"
|
||||
"026.Tafsir.Gooya.Shuara"
|
||||
"027.Tafsir.Gooya.Naml"
|
||||
"028.Tafsir.Gooya.Qasas"
|
||||
"029.Tafsir.Gooya.Ankaboot"
|
||||
"030.Tafsir.Gooya.Room"
|
||||
"031.Tafsir.Gooya.Luqman"
|
||||
"032.Tafsir.Gooya.Sajda"
|
||||
"033.Tafsir.Gooya.Ahzab"
|
||||
"034.Tafsir.Gooya.Saba"
|
||||
"035.Tafsir.Gooya.Fatir"
|
||||
"036.Tafsir.Gooya.Yaseen"
|
||||
"037.Tafsir.Gooya.Saaffat"
|
||||
"038.Tafsir.Gooya.Sad"
|
||||
"039.Tafsir.Gooya.Zumar"
|
||||
"040.Tafsir.Gooya.Ghafir"
|
||||
"041.Tafsir.Gooya.Fussilat"
|
||||
"042.Tafsir.Gooya.Shura"
|
||||
"043.Tafsir.Gooya.Zukhruf"
|
||||
"044.Tafsir.Gooya.Dukhan"
|
||||
"045.Tafsir.Gooya.Jathiya"
|
||||
"046.Tafsir.Gooya.Ahqaf"
|
||||
"047.Tafsir.Gooya.Muhammad"
|
||||
"048.Tafsir.Gooya.Fath"
|
||||
"049.Tafsir.Gooya.Hujaraat"
|
||||
"050.Tafsir.Gooya.Qaf"
|
||||
"051.Tafsir.Gooya.Dhariyat"
|
||||
"052.Tafsir.Gooya.Tur"
|
||||
"053.Tafsir.Gooya.Najm"
|
||||
"054.Tafsir.Gooya.Qamar"
|
||||
"055.Tafsir.Gooya.Ar-Rahman"
|
||||
"056.Tafsir.Gooya.Waqia"
|
||||
"057.Tafsir.Gooya.Hadid"
|
||||
"058.Tafsir.Gooya.Mujadila"
|
||||
"059.Tafsir.Gooya.Hashr"
|
||||
"060.Tafsir.Gooya.Mumtahina"
|
||||
"061.Tafsir.Gooya.Saff"
|
||||
"062.Tafsir.Gooya.Jumua"
|
||||
"063.Tafsir.Gooya.Munafiqoon"
|
||||
"064.Tafsir.Gooya.Taghabun"
|
||||
"065.Tafsir.Gooya.Talaq"
|
||||
"066.Tafsir.Gooya.Tahrim"
|
||||
"067.Tafsir.Gooya.Mulk"
|
||||
"068.Tafsir.Gooya.Qalam"
|
||||
"069.Tafsir.Gooya.Haaqqa"
|
||||
"070.Tafsir.Gooya.Maarij"
|
||||
"071.Tafsir.Gooya.Nooh"
|
||||
"072.Tafsir.Gooya.Jinn"
|
||||
"073.Tafsir.Gooya.Muzzammil"
|
||||
"074.Tafsir.Gooya.muddaththir"
|
||||
"075.Tafsir.Gooya.Qiyama"
|
||||
"076.Tafsir.Gooya.Insan"
|
||||
"077.Tafsir.Gooya.Mursalat"
|
||||
"078.Tafsir.Gooya.Naba"
|
||||
"079.Tafsir.Gooya.Naziat"
|
||||
"080.Tafsir.Gooya.Abasa"
|
||||
"081.Tafsir.Gooya.Takwir"
|
||||
"082.Tafsir.Gooya.Infitar"
|
||||
"083.Tafsir.Gooya.Mutaffifin"
|
||||
"084.Tafsir.Gooya.Inshiqaq"
|
||||
"085.Tafsir.Gooya.Brooj"
|
||||
"086.Tafsir.Gooya.Tariq"
|
||||
"087.Tafsir.Gooya.Ala"
|
||||
"088.Tafsir.Gooya.Ghashiya"
|
||||
"089.Tafsir.Gooya.Fajr"
|
||||
"090.Tafsir.Gooya.Balad"
|
||||
"091.Tafsir.Gooya.Shams"
|
||||
"092.Tafsir.Gooya.Lail"
|
||||
"093.Tafsir.Gooya.Dhuha"
|
||||
"094.Tafsir.Gooya.Inshirah"
|
||||
"095.Tafsir.Gooya.Tin"
|
||||
"096.Tafsir.Gooya.Alaq"
|
||||
"097.Tafsir.Gooya.Qadr"
|
||||
"098.Tafsir.Gooya.Bayyina"
|
||||
"099.Tafsir.Gooya.Zalzala"
|
||||
"100.Tafsir.Gooya.Adiyat"
|
||||
"101.Tafsir.Gooya.Qaria"
|
||||
"102.Tafsir.Gooya.Takathur"
|
||||
"103.Tafsir.Gooya.Asr"
|
||||
"104.Tafsir.Gooya.Humaza"
|
||||
"105.Tafsir.Gooya.Fil"
|
||||
"106.Tafsir.Gooya.Quarish"
|
||||
"107.Tafsir.Gooya.Maun"
|
||||
"108.Tafsir.Gooya.kauther"
|
||||
"109.Tafsir.Gooya.Kafiroon"
|
||||
"110.Tafsir.Gooya.Nasr"
|
||||
"111.Tafsir.Gooya.Masad"
|
||||
"112.Tafsir.Gooya.Ikhlas"
|
||||
"113.Tafsir.Gooya.Falaq"
|
||||
"114.Tafsir.Gooya.Nas"
|
||||
)
|
||||
|
||||
extract_one() {
|
||||
local rar="$1"
|
||||
local dest="$2"
|
||||
mkdir -p "$dest"
|
||||
if command -v unrar >/dev/null 2>&1; then
|
||||
unrar x -o+ -p"${PASSWORD}" "$rar" "$dest/"
|
||||
else
|
||||
7z x -y "-p${PASSWORD}" "-o${dest}" "$rar"
|
||||
fi
|
||||
}
|
||||
|
||||
for slug in "${SLUGS[@]}"; do
|
||||
num="${slug%%.*}"
|
||||
out_dir="${AUDIO_DIR}/${num}"
|
||||
# skip if already has audio
|
||||
if find "$out_dir" -type f \( -iname '*.mp3' -o -iname '*.m4a' -o -iname '*.ogg' \) 2>/dev/null | grep -q .; then
|
||||
echo "==> skip ${num} (already extracted)"
|
||||
continue
|
||||
fi
|
||||
|
||||
rar="${WORKDIR}/${slug}.rar"
|
||||
url="${BASE_URL}/${slug}.rar"
|
||||
echo "==> download ${slug}"
|
||||
curl -fL --retry 5 --retry-delay 3 -o "$rar" "$url"
|
||||
|
||||
echo "==> extract ${slug} -> ${out_dir}"
|
||||
extract_one "$rar" "$out_dir"
|
||||
rm -f "$rar"
|
||||
echo "==> done ${num}"
|
||||
done
|
||||
|
||||
rmdir "$WORKDIR" 2>/dev/null || true
|
||||
echo "audio import finished under ${AUDIO_DIR}"
|
||||
find "$AUDIO_DIR" -type f \( -iname '*.mp3' -o -iname '*.m4a' \) | wc -l
|
||||
@@ -56,23 +56,33 @@ public static class TafsirEndpoints
|
||||
return Results.Ok(detail);
|
||||
});
|
||||
|
||||
group.MapGet("/audio/{episodeId:guid}", async (Guid episodeId, AppDbContext db, IWebHostEnvironment env, CancellationToken ct) =>
|
||||
group.MapGet("/audio/{episodeId:guid}", async (Guid episodeId, AppDbContext db, IConfiguration config, IWebHostEnvironment env, CancellationToken ct) =>
|
||||
{
|
||||
var episode = await db.Episodes.AsNoTracking().FirstOrDefaultAsync(e => e.Id == episodeId, ct);
|
||||
if (episode is null) return Results.NotFound();
|
||||
|
||||
var relative = episode.AudioUrl.TrimStart('/');
|
||||
var path = Path.Combine(env.ContentRootPath, "wwwroot", relative);
|
||||
if (!File.Exists(path))
|
||||
var audioRoot = config["AudioRoot"] ?? "/data/audio";
|
||||
var relative = episode.AudioUrl.TrimStart('/').Replace('\\', '/');
|
||||
|
||||
// Prefer PVC mount; fallback to wwwroot for local sample files
|
||||
var candidates = new[]
|
||||
{
|
||||
// Fallback placeholder: return empty audio-friendly response path for demo
|
||||
var placeholder = Path.Combine(env.ContentRootPath, "wwwroot", "audio", "silence.mp3");
|
||||
if (File.Exists(placeholder))
|
||||
return Results.File(placeholder, "audio/mpeg", enableRangeProcessing: true);
|
||||
return Results.NotFound();
|
||||
Path.Combine(audioRoot, relative),
|
||||
Path.Combine(env.ContentRootPath, "wwwroot", relative),
|
||||
Path.Combine(env.ContentRootPath, "wwwroot", "audio", Path.GetFileName(relative))
|
||||
};
|
||||
|
||||
foreach (var path in candidates)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
return Results.File(path, "audio/mpeg", enableRangeProcessing: true);
|
||||
}
|
||||
|
||||
return Results.File(path, "audio/mpeg", enableRangeProcessing: true);
|
||||
var silence = Path.Combine(env.ContentRootPath, "wwwroot", "audio", "silence.mp3");
|
||||
if (File.Exists(silence))
|
||||
return Results.File(silence, "audio/mpeg", enableRangeProcessing: true);
|
||||
|
||||
return Results.NotFound();
|
||||
});
|
||||
|
||||
return group;
|
||||
|
||||
@@ -47,7 +47,8 @@ using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
try
|
||||
{
|
||||
await DbSeeder.SeedAsync(db);
|
||||
var audioRoot = app.Configuration["AudioRoot"] ?? "/data/audio";
|
||||
await DbSeeder.SeedAsync(db, audioRoot);
|
||||
break;
|
||||
}
|
||||
catch (Exception ex) when (attempt < 30)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5434;Database=tafsirquran;Username=tafsir;Password=tafsir_dev"
|
||||
"Default": "Server=localhost,31433;Database=TafsirQuran;User Id=sa;Password=87zH26nbqT;TrustServerCertificate=True"
|
||||
},
|
||||
"AudioRoot": "/data/audio",
|
||||
"Jwt": {
|
||||
"Key": "TafsirQuran-Dev-Secret-Key-Change-In-Production-32+",
|
||||
"Issuer": "TafsirQuran",
|
||||
|
||||
@@ -10,10 +10,10 @@ public static class DependencyInjection
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString("Default")
|
||||
?? "Host=localhost;Port=5434;Database=tafsirquran;Username=tafsir;Password=tafsir_dev";
|
||||
?? "Server=localhost,31433;Database=TafsirQuran;User Id=sa;Password=87zH26nbqT;TrustServerCertificate=True";
|
||||
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseNpgsql(connectionString));
|
||||
options.UseSqlServer(connectionString));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -7,114 +7,154 @@ public static class DbSeeder
|
||||
{
|
||||
public static readonly Guid GhatrehChekaniId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
public static async Task SeedAsync(AppDbContext db, CancellationToken ct = default)
|
||||
private static readonly (string Ar, string Fa)[] SurahNames =
|
||||
[
|
||||
("الفاتحة", "فاتحه"), ("البقرة", "بقره"), ("آل عمران", "آل عمران"), ("النساء", "نساء"),
|
||||
("المائدة", "مائده"), ("الأنعام", "انعام"), ("الأعراف", "اعراف"), ("الأنفال", "انفال"),
|
||||
("التوبة", "توبه"), ("يونس", "یونس"), ("هود", "هود"), ("يوسف", "یوسف"),
|
||||
("الرعد", "رعد"), ("إبراهيم", "ابراهیم"), ("الحجر", "حجر"), ("النحل", "نحل"),
|
||||
("الإسراء", "اسراء"), ("الكهف", "کهف"), ("مريم", "مریم"), ("طه", "طه"),
|
||||
("الأنبياء", "انبیاء"), ("الحج", "حج"), ("المؤمنون", "مؤمنون"), ("النور", "نور"),
|
||||
("الفرقان", "فرقان"), ("الشعراء", "شعراء"), ("النمل", "نمل"), ("القصص", "قصص"),
|
||||
("العنكبوت", "عنکبوت"), ("الروم", "روم"), ("لقمان", "لقمان"), ("السجدة", "سجده"),
|
||||
("الأحزاب", "احزاب"), ("سبأ", "سبأ"), ("فاطر", "فاطر"), ("يس", "یس"),
|
||||
("الصافات", "صافات"), ("ص", "ص"), ("الزمر", "زمر"), ("غافر", "غافر"),
|
||||
("فصلت", "فصلت"), ("الشورى", "شوری"), ("الزخرف", "زخرف"), ("الدخان", "دخان"),
|
||||
("الجاثية", "جاثیه"), ("الأحقاف", "احقاف"), ("محمد", "محمد"), ("الفتح", "فتح"),
|
||||
("الحجرات", "حجرات"), ("ق", "ق"), ("الذاريات", "ذاریات"), ("الطور", "طور"),
|
||||
("النجم", "نجم"), ("القمر", "قمر"), ("الرحمن", "رحمن"), ("الواقعة", "واقعه"),
|
||||
("الحديد", "حدید"), ("المجادلة", "مجادله"), ("الحشر", "حشر"), ("الممتحنة", "ممتحنه"),
|
||||
("الصف", "صف"), ("الجمعة", "جمعه"), ("المنافقون", "منافقون"), ("التغابن", "تغابن"),
|
||||
("الطلاق", "طلاق"), ("التحريم", "تحریم"), ("الملك", "ملک"), ("القلم", "قلم"),
|
||||
("الحاقة", "حاقه"), ("المعارج", "معارج"), ("نوح", "نوح"), ("الجن", "جن"),
|
||||
("المزمل", "مزمل"), ("المدثر", "مدثر"), ("القيامة", "قیامه"), ("الإنسان", "انسان"),
|
||||
("المرسلات", "مرسلات"), ("النبأ", "نبأ"), ("النازعات", "نازعات"), ("عبس", "عبس"),
|
||||
("التكوير", "تکویر"), ("الانفطار", "انفطار"), ("المطففين", "مطففین"), ("الانشقاق", "انشقاق"),
|
||||
("البروج", "بروج"), ("الطارق", "طارق"), ("الأعلى", "اعلی"), ("الغاشية", "غاشیه"),
|
||||
("الفجر", "فجر"), ("البلد", "بلد"), ("الشمس", "شمس"), ("الليل", "لیل"),
|
||||
("الضحى", "ضحی"), ("الشرح", "شرح"), ("التين", "تین"), ("العلق", "علق"),
|
||||
("القدر", "قدر"), ("البينة", "بینه"), ("الزلزلة", "زلزله"), ("العاديات", "عادیات"),
|
||||
("القارعة", "قارعه"), ("التكاثر", "تکاثر"), ("العصر", "عصر"), ("الهمزة", "همزه"),
|
||||
("الفيل", "فیل"), ("قريش", "قریش"), ("الماعون", "ماعون"), ("الكوثر", "کوثر"),
|
||||
("الكافرون", "کافرون"), ("النصر", "نصر"), ("المسد", "مسد"), ("الإخلاص", "اخلاص"),
|
||||
("الفلق", "فلق"), ("الناس", "ناس")
|
||||
];
|
||||
|
||||
public static async Task SeedAsync(AppDbContext db, string? audioRoot = null, CancellationToken ct = default)
|
||||
{
|
||||
await db.Database.MigrateAsync(ct);
|
||||
|
||||
if (!await db.Surahs.AnyAsync(ct))
|
||||
{
|
||||
db.Surahs.AddRange(
|
||||
new Surah { Id = 1, Number = 1, NameAr = "الفاتحة", NameFa = "فاتحه" },
|
||||
new Surah { Id = 2, Number = 2, NameAr = "البقرة", NameFa = "بقره" },
|
||||
new Surah { Id = 112, Number = 112, NameAr = "الإخلاص", NameFa = "اخلاص" }
|
||||
);
|
||||
|
||||
db.Ayahs.AddRange(
|
||||
new Ayah
|
||||
for (var i = 0; i < SurahNames.Length; i++)
|
||||
{
|
||||
var n = i + 1;
|
||||
db.Surahs.Add(new Surah
|
||||
{
|
||||
SurahId = 1,
|
||||
Number = 1,
|
||||
TextAr = "بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ",
|
||||
TextFa = "به نام خداوند بخشنده مهربان"
|
||||
},
|
||||
new Ayah
|
||||
{
|
||||
SurahId = 1,
|
||||
Number = 2,
|
||||
TextAr = "الْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ",
|
||||
TextFa = "ستایش مخصوص خداوندی است که پروردگار جهانیان است"
|
||||
},
|
||||
new Ayah
|
||||
{
|
||||
SurahId = 1,
|
||||
Number = 3,
|
||||
TextAr = "الرَّحْمَٰنِ الرَّحِيمِ",
|
||||
TextFa = "بخشنده مهربان"
|
||||
},
|
||||
new Ayah
|
||||
{
|
||||
SurahId = 112,
|
||||
Number = 1,
|
||||
TextAr = "قُلْ هُوَ اللَّهُ أَحَدٌ",
|
||||
TextFa = "بگو اوست خدای یگانه"
|
||||
}
|
||||
);
|
||||
Id = n,
|
||||
Number = n,
|
||||
NameAr = SurahNames[i].Ar,
|
||||
NameFa = SurahNames[i].Fa
|
||||
});
|
||||
}
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
if (!await db.Tafsirs.AnyAsync(ct))
|
||||
{
|
||||
var tafsir = new Tafsir
|
||||
db.Tafsirs.Add(new Tafsir
|
||||
{
|
||||
Id = GhatrehChekaniId,
|
||||
Title = "تفسیر قطره چکانی",
|
||||
Author = "قرائتی",
|
||||
Slug = "ghatreh-chekani",
|
||||
IsPublished = true
|
||||
};
|
||||
db.Tafsirs.Add(tafsir);
|
||||
|
||||
db.Episodes.AddRange(
|
||||
new TafsirEpisode
|
||||
{
|
||||
Id = Guid.Parse("22222222-2222-2222-2222-222222222201"),
|
||||
TafsirId = GhatrehChekaniId,
|
||||
SurahId = 1,
|
||||
AyahNumber = 1,
|
||||
Title = "فاتحه آیه ۱",
|
||||
AudioUrl = "/audio/sample-1.mp3",
|
||||
DurationSeconds = 180,
|
||||
SortOrder = 1,
|
||||
Summary = "تفسیر قطرهچکانی سوره فاتحه آیه اول"
|
||||
},
|
||||
new TafsirEpisode
|
||||
{
|
||||
Id = Guid.Parse("22222222-2222-2222-2222-222222222202"),
|
||||
TafsirId = GhatrehChekaniId,
|
||||
SurahId = 1,
|
||||
AyahNumber = 2,
|
||||
Title = "فاتحه آیه ۲",
|
||||
AudioUrl = "/audio/sample-2.mp3",
|
||||
DurationSeconds = 210,
|
||||
SortOrder = 2,
|
||||
Summary = "تفسیر قطرهچکانی سوره فاتحه آیه دوم"
|
||||
},
|
||||
new TafsirEpisode
|
||||
{
|
||||
Id = Guid.Parse("22222222-2222-2222-2222-222222222203"),
|
||||
TafsirId = GhatrehChekaniId,
|
||||
SurahId = 1,
|
||||
AyahNumber = 3,
|
||||
Title = "فاتحه آیه ۳",
|
||||
AudioUrl = "/audio/sample-3.mp3",
|
||||
DurationSeconds = 195,
|
||||
SortOrder = 3,
|
||||
Summary = "تفسیر قطرهچکانی سوره فاتحه آیه سوم"
|
||||
},
|
||||
new TafsirEpisode
|
||||
{
|
||||
Id = Guid.Parse("22222222-2222-2222-2222-222222222204"),
|
||||
TafsirId = GhatrehChekaniId,
|
||||
SurahId = 112,
|
||||
AyahNumber = 1,
|
||||
Title = "اخلاص آیه ۱",
|
||||
AudioUrl = "/audio/sample-4.mp3",
|
||||
DurationSeconds = 160,
|
||||
SortOrder = 4,
|
||||
Summary = "تفسیر قطرهچکانی سوره اخلاص آیه اول"
|
||||
}
|
||||
);
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
await SyncEpisodesFromAudioAsync(db, audioRoot, ct);
|
||||
}
|
||||
|
||||
public static async Task SyncEpisodesFromAudioAsync(AppDbContext db, string? audioRoot, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(audioRoot) || !Directory.Exists(audioRoot))
|
||||
return;
|
||||
|
||||
var tafsir = await db.Tafsirs.FirstOrDefaultAsync(t => t.Id == GhatrehChekaniId, ct);
|
||||
if (tafsir is null) return;
|
||||
|
||||
var existing = await db.Episodes
|
||||
.Where(e => e.TafsirId == GhatrehChekaniId)
|
||||
.Select(e => e.AudioUrl)
|
||||
.ToListAsync(ct);
|
||||
var existingSet = existing.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var audioFiles = Directory.EnumerateFiles(audioRoot, "*.*", SearchOption.AllDirectories)
|
||||
.Where(f => f.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase)
|
||||
|| f.EndsWith(".m4a", StringComparison.OrdinalIgnoreCase)
|
||||
|| f.EndsWith(".ogg", StringComparison.OrdinalIgnoreCase)
|
||||
|| f.EndsWith(".wav", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (audioFiles.Count == 0) return;
|
||||
|
||||
var sort = existing.Count;
|
||||
var added = 0;
|
||||
foreach (var file in audioFiles)
|
||||
{
|
||||
var relative = Path.GetRelativePath(audioRoot, file).Replace('\\', '/');
|
||||
if (existingSet.Contains(relative)) continue;
|
||||
|
||||
var surahNumber = GuessSurahNumber(relative, file);
|
||||
var ayahNumber = GuessAyahNumber(Path.GetFileNameWithoutExtension(file));
|
||||
sort++;
|
||||
db.Episodes.Add(new TafsirEpisode
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TafsirId = GhatrehChekaniId,
|
||||
SurahId = surahNumber,
|
||||
AyahNumber = ayahNumber,
|
||||
Title = $"{SurahNames[surahNumber - 1].Fa} — {Path.GetFileNameWithoutExtension(file)}",
|
||||
AudioUrl = relative,
|
||||
DurationSeconds = 0,
|
||||
SortOrder = sort,
|
||||
Summary = null
|
||||
});
|
||||
added++;
|
||||
}
|
||||
|
||||
if (added > 0)
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
private static int GuessSurahNumber(string relative, string fullPath)
|
||||
{
|
||||
var parts = relative.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
var digits = new string(part.TakeWhile(char.IsDigit).ToArray());
|
||||
if (digits.Length >= 1 && int.TryParse(digits, out var n) && n is >= 1 and <= 114)
|
||||
return n;
|
||||
}
|
||||
|
||||
var name = Path.GetFileName(fullPath);
|
||||
var nameDigits = new string(name.TakeWhile(char.IsDigit).ToArray());
|
||||
if (nameDigits.Length >= 1 && int.TryParse(nameDigits, out var fromName) && fromName is >= 1 and <= 114)
|
||||
return fromName;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int GuessAyahNumber(string fileName)
|
||||
{
|
||||
// patterns: ..._001, -1, .1, ayah01
|
||||
var matches = System.Text.RegularExpressions.Regex.Matches(fileName, @"(\d+)");
|
||||
if (matches.Count == 0) return 1;
|
||||
// prefer last number group as ayah when multiple
|
||||
if (int.TryParse(matches[^1].Value, out var n) && n > 0)
|
||||
return n;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
+56
-54
@@ -2,9 +2,9 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
@@ -12,8 +12,8 @@ using TafsirQuran.Infrastructure.Persistence;
|
||||
namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20261009174748_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
[Migration("20261009190049_InitialSqlServer")]
|
||||
partial class InitialSqlServer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -21,32 +21,32 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("TafsirQuran.Domain.Entities.AppUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -60,20 +60,20 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SurahId")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TextAr")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("TextFa")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -87,25 +87,25 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid>("EpisodeId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -114,7 +114,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("DeviceId", "UserId", "EpisodeId")
|
||||
.IsUnique();
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Bookmarks");
|
||||
});
|
||||
@@ -123,21 +124,21 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid>("EpisodeId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -146,7 +147,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("DeviceId", "UserId", "EpisodeId")
|
||||
.IsUnique();
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Favorites");
|
||||
});
|
||||
@@ -155,24 +157,24 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid>("EpisodeId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double>("PositionSeconds")
|
||||
.HasColumnType("double precision");
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -189,22 +191,22 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("NameAr")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("NameFa")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -218,28 +220,28 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsPublished")
|
||||
.HasColumnType("boolean");
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -253,34 +255,34 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("AudioUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("AyahNumber")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DurationSeconds")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("SurahId")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("TafsirId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
.HasColumnType("nvarchar(300)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
+52
-51
@@ -1,13 +1,12 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
public partial class InitialSqlServer : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
@@ -16,11 +15,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
name: "Surahs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Number = table.Column<int>(type: "integer", nullable: false),
|
||||
NameAr = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
|
||||
NameFa = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false)
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Number = table.Column<int>(type: "int", nullable: false),
|
||||
NameAr = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false),
|
||||
NameFa = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -31,12 +30,12 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
name: "Tafsirs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Author = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
Slug = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
|
||||
CoverUrl = table.Column<string>(type: "text", nullable: true),
|
||||
IsPublished = table.Column<bool>(type: "boolean", nullable: false)
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Author = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false),
|
||||
Slug = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false),
|
||||
CoverUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsPublished = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -47,11 +46,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: false),
|
||||
DisplayName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
DisplayName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -62,11 +61,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
name: "Ayahs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
SurahId = table.Column<int>(type: "integer", nullable: false),
|
||||
Number = table.Column<int>(type: "integer", nullable: false),
|
||||
TextAr = table.Column<string>(type: "text", nullable: false),
|
||||
TextFa = table.Column<string>(type: "text", nullable: true)
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SurahId = table.Column<int>(type: "int", nullable: false),
|
||||
Number = table.Column<int>(type: "int", nullable: false),
|
||||
TextAr = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
TextFa = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -83,15 +82,15 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
name: "Episodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TafsirId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
SurahId = table.Column<int>(type: "integer", nullable: false),
|
||||
AyahNumber = table.Column<int>(type: "integer", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
|
||||
AudioUrl = table.Column<string>(type: "text", nullable: false),
|
||||
DurationSeconds = table.Column<int>(type: "integer", nullable: false),
|
||||
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||
Summary = table.Column<string>(type: "text", nullable: true)
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TafsirId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SurahId = table.Column<int>(type: "int", nullable: false),
|
||||
AyahNumber = table.Column<int>(type: "int", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: false),
|
||||
AudioUrl = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
DurationSeconds = table.Column<int>(type: "int", nullable: false),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false),
|
||||
Summary = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -114,12 +113,12 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
name: "Bookmarks",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
DeviceId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
EpisodeId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Note = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeviceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
EpisodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Note = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -141,11 +140,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
name: "Favorites",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
DeviceId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
EpisodeId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeviceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
EpisodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -167,12 +166,12 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
name: "ListeningProgresses",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
DeviceId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
EpisodeId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PositionSeconds = table.Column<double>(type: "double precision", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
DeviceId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
EpisodeId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
PositionSeconds = table.Column<double>(type: "float", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -200,7 +199,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
name: "IX_Bookmarks_DeviceId_UserId_EpisodeId",
|
||||
table: "Bookmarks",
|
||||
columns: new[] { "DeviceId", "UserId", "EpisodeId" },
|
||||
unique: true);
|
||||
unique: true,
|
||||
filter: "[UserId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Bookmarks_EpisodeId",
|
||||
@@ -226,7 +226,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
name: "IX_Favorites_DeviceId_UserId_EpisodeId",
|
||||
table: "Favorites",
|
||||
columns: new[] { "DeviceId", "UserId", "EpisodeId" },
|
||||
unique: true);
|
||||
unique: true,
|
||||
filter: "[UserId] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Favorites_EpisodeId",
|
||||
+54
-52
@@ -2,8 +2,8 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
@@ -18,32 +18,32 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("TafsirQuran.Domain.Entities.AppUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
.HasColumnType("nvarchar(128)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -57,20 +57,20 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SurahId")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TextAr")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("TextFa")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -84,25 +84,25 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid>("EpisodeId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -111,7 +111,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("DeviceId", "UserId", "EpisodeId")
|
||||
.IsUnique();
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Bookmarks");
|
||||
});
|
||||
@@ -120,21 +121,21 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid>("EpisodeId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -143,7 +144,8 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("DeviceId", "UserId", "EpisodeId")
|
||||
.IsUnique();
|
||||
.IsUnique()
|
||||
.HasFilter("[UserId] IS NOT NULL");
|
||||
|
||||
b.ToTable("Favorites");
|
||||
});
|
||||
@@ -152,24 +154,24 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<Guid>("EpisodeId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<double>("PositionSeconds")
|
||||
.HasColumnType("double precision");
|
||||
.HasColumnType("float");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -186,22 +188,22 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("NameAr")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<string>("NameFa")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
.HasColumnType("nvarchar(80)");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -215,28 +217,28 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsPublished")
|
||||
.HasColumnType("boolean");
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
.HasColumnType("nvarchar(120)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
@@ -250,34 +252,34 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("AudioUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("AyahNumber")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("DurationSeconds")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("text");
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("SurahId")
|
||||
.HasColumnType("integer");
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("TafsirId")
|
||||
.HasColumnType("uuid");
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
.HasColumnType("nvarchar(300)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
@using TafsirQuran.UI
|
||||
@using TafsirQuran.UI.Layout
|
||||
|
||||
<Router AppAssembly="typeof(Routes).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(AppLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="typeof(AppLayout)">
|
||||
<p class="muted">صفحه پیدا نشد.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
@@ -1,9 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
@Body
|
||||
|
||||
<div id="blazor-error-ui" data-nosnippet>
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
@@ -1,20 +0,0 @@
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
@@ -1,30 +1,13 @@
|
||||
using TafsirQuran.UI;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using TafsirQuran.UI.Services;
|
||||
using TafsirQuran.Web.Components;
|
||||
using TafsirQuran.Web;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
var apiBase = builder.Configuration["ApiBaseAddress"] ?? "http://localhost:5080/";
|
||||
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
builder.Services.AddTafsirUi(apiBase);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
}
|
||||
|
||||
// HTTP NodePort / reverse-proxy; do not force HTTPS redirect in cluster.
|
||||
app.UseAntiforgery();
|
||||
app.UseStaticFiles();
|
||||
app.MapStaticAssets();
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddAdditionalAssemblies(typeof(TafsirQuran.UI.Routes).Assembly);
|
||||
|
||||
app.Run();
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5299",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7169;http://localhost:5299",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "http://localhost:5299",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.10" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\TafsirQuran.UI\TafsirQuran.UI.csproj" />
|
||||
<ProjectReference Include="..\TafsirQuran.Shared\TafsirQuran.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using TafsirQuran.UI
|
||||
@using TafsirQuran.UI.Layout
|
||||
@using TafsirQuran.UI.Pages
|
||||
@using TafsirQuran.UI.Components
|
||||
@using TafsirQuran.Web
|
||||
@using TafsirQuran.Web.Components
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"ApiBaseAddress": "http://localhost:5080/",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ApiBaseAddress": "http://localhost:5080/"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"ApiBaseAddress": "http://185.252.31.44:30821/"
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
|
||||
<head>
|
||||
@@ -6,19 +6,22 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#1f6b4a" />
|
||||
<meta name="description" content="تفسیر قرآن کریم — صوت و متن" />
|
||||
<title>تفسیر قرآن</title>
|
||||
<base href="/" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<link rel="apple-touch-icon" href="icons/icon-192.png" />
|
||||
<link rel="stylesheet" href="_content/TafsirQuran.UI/css/app.css" />
|
||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||
<HeadOutlet @rendermode="InteractiveServer" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
<link href="TafsirQuran.Web.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<TafsirQuran.UI.Routes @rendermode="InteractiveServer" />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<div id="app">
|
||||
<div class="muted" style="padding:2rem;text-align:center">در حال بارگذاری...</div>
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script src="_content/TafsirQuran.UI/js/app.js"></script>
|
||||
<script src="service-worker-register.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,5 +0,0 @@
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/service-worker.js').catch(() => {});
|
||||
});
|
||||
}
|
||||
@@ -1,25 +1,2 @@
|
||||
const CACHE = 'tafsirquran-static-v1';
|
||||
const ASSETS = [
|
||||
'/',
|
||||
'/manifest.webmanifest',
|
||||
'/_content/TafsirQuran.UI/css/app.css',
|
||||
'/_content/TafsirQuran.UI/js/app.js'
|
||||
];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(ASSETS)).then(() => self.skipWaiting()));
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const req = event.request;
|
||||
if (req.method !== 'GET') return;
|
||||
event.respondWith(
|
||||
caches.match(req).then((cached) => cached || fetch(req).catch(() => caches.match('/')))
|
||||
);
|
||||
});
|
||||
// Development placeholder — published SW is service-worker.published.js
|
||||
self.addEventListener('fetch', () => { });
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Cache static WASM assets only — never HTML navigations.
|
||||
const CACHE = 'tafsirquran-wasm-v1';
|
||||
|
||||
self.addEventListener('install', event => event.waitUntil(self.skipWaiting()));
|
||||
self.addEventListener('activate', event => event.waitUntil(
|
||||
caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))))
|
||||
.then(() => self.clients.claim())
|
||||
));
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
const req = event.request;
|
||||
if (req.method !== 'GET') return;
|
||||
const url = new URL(req.url);
|
||||
if (req.mode === 'navigate' || (req.headers.get('accept') || '').includes('text/html')) {
|
||||
event.respondWith(fetch(req));
|
||||
return;
|
||||
}
|
||||
if (url.pathname.startsWith('/api/') || url.origin.includes(':30821')) {
|
||||
event.respondWith(fetch(req));
|
||||
return;
|
||||
}
|
||||
event.respondWith(
|
||||
caches.open(CACHE).then(async cache => {
|
||||
const cached = await cache.match(req);
|
||||
if (cached) return cached;
|
||||
const res = await fetch(req);
|
||||
if (res.ok) cache.put(req, res.clone());
|
||||
return res;
|
||||
})
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user