Migrate to Blazor WASM, MSSQL platform DB, and durable audio PVC.
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:
2026-10-09 22:40:53 +03:30
parent 7f06856f45
commit ae746d65ba
32 changed files with 760 additions and 525 deletions
+7 -7
View File
@@ -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
+10 -2
View File
@@ -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
```
## پیش‌نیاز توسعه محلی
+16
View File
@@ -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;
}
}
+104
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+171
View File
@@ -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;
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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;
}
}
@@ -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");
@@ -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",
@@ -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>
+14
View File
@@ -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;
}
+7 -24
View File
@@ -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"
}
}
}
}
+11 -1
View File
@@ -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"
}
}
}
-10
View File
@@ -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(() => {});
});
}
+2 -25
View File
@@ -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;
})
);
});