diff --git a/Dockerfile.web b/Dockerfile.web index 6db0185..9cbd8f8 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -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 diff --git a/README.md b/README.md index 24860cb..b2e422e 100644 --- a/README.md +++ b/README.md @@ -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 +``` ## پیش‌نیاز توسعه محلی diff --git a/docker/nginx-web.conf b/docker/nginx-web.conf new file mode 100644 index 0000000..90370b5 --- /dev/null +++ b/docker/nginx-web.conf @@ -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; + } +} diff --git a/k8s/audio-import-job.yaml b/k8s/audio-import-job.yaml new file mode 100644 index 0000000..0d7e881 --- /dev/null +++ b/k8s/audio-import-job.yaml @@ -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) diff --git a/k8s/manifest.yaml b/k8s/manifest.yaml index d277581..26076d9 100644 --- a/k8s/manifest.yaml +++ b/k8s/manifest.yaml @@ -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 diff --git a/scripts/cicd-deploy.sh b/scripts/cicd-deploy.sh index 61d15ed..4f5fb0e 100755 --- a/scripts/cicd-deploy.sh +++ b/scripts/cicd-deploy.sh @@ -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" diff --git a/scripts/fetch-audio.sh b/scripts/fetch-audio.sh new file mode 100755 index 0000000..9fca1b6 --- /dev/null +++ b/scripts/fetch-audio.sh @@ -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 diff --git a/src/TafsirQuran.Api/Endpoints/TafsirEndpoints.cs b/src/TafsirQuran.Api/Endpoints/TafsirEndpoints.cs index 0bfd127..19deb2b 100644 --- a/src/TafsirQuran.Api/Endpoints/TafsirEndpoints.cs +++ b/src/TafsirQuran.Api/Endpoints/TafsirEndpoints.cs @@ -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; diff --git a/src/TafsirQuran.Api/Program.cs b/src/TafsirQuran.Api/Program.cs index 7daa76a..afd2705 100644 --- a/src/TafsirQuran.Api/Program.cs +++ b/src/TafsirQuran.Api/Program.cs @@ -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) diff --git a/src/TafsirQuran.Api/appsettings.json b/src/TafsirQuran.Api/appsettings.json index 9d5c7cf..2eaccba 100644 --- a/src/TafsirQuran.Api/appsettings.json +++ b/src/TafsirQuran.Api/appsettings.json @@ -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", diff --git a/src/TafsirQuran.Infrastructure/DependencyInjection.cs b/src/TafsirQuran.Infrastructure/DependencyInjection.cs index 537c2d6..f8a3326 100644 --- a/src/TafsirQuran.Infrastructure/DependencyInjection.cs +++ b/src/TafsirQuran.Infrastructure/DependencyInjection.cs @@ -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(options => - options.UseNpgsql(connectionString)); + options.UseSqlServer(connectionString)); return services; } diff --git a/src/TafsirQuran.Infrastructure/Persistence/DbSeeder.cs b/src/TafsirQuran.Infrastructure/Persistence/DbSeeder.cs index 4b25cae..1ace426 100644 --- a/src/TafsirQuran.Infrastructure/Persistence/DbSeeder.cs +++ b/src/TafsirQuran.Infrastructure/Persistence/DbSeeder.cs @@ -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; } } diff --git a/src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009174748_InitialCreate.Designer.cs b/src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009190049_InitialSqlServer.Designer.cs similarity index 77% rename from src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009174748_InitialCreate.Designer.cs rename to src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009190049_InitialSqlServer.Designer.cs index 9da63e1..84c18d6 100644 --- a/src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009174748_InitialCreate.Designer.cs +++ b/src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009190049_InitialSqlServer.Designer.cs @@ -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 { /// 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("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetimeoffset"); b.Property("DisplayName") .IsRequired() .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("nvarchar(128)"); b.Property("Email") .IsRequired() .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("nvarchar(256)"); b.Property("PasswordHash") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -60,20 +60,20 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Number") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("SurahId") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("TextAr") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("TextFa") - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -87,25 +87,25 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetimeoffset"); b.Property("DeviceId") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("nvarchar(64)"); b.Property("EpisodeId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Note") .HasMaxLength(500) - .HasColumnType("character varying(500)"); + .HasColumnType("nvarchar(500)"); b.Property("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("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetimeoffset"); b.Property("DeviceId") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("nvarchar(64)"); b.Property("EpisodeId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("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("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("DeviceId") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("nvarchar(64)"); b.Property("EpisodeId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("PositionSeconds") - .HasColumnType("double precision"); + .HasColumnType("float"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetimeoffset"); b.Property("UserId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.HasKey("Id"); @@ -189,22 +191,22 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("integer"); + .HasColumnType("int"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("NameAr") .IsRequired() .HasMaxLength(80) - .HasColumnType("character varying(80)"); + .HasColumnType("nvarchar(80)"); b.Property("NameFa") .IsRequired() .HasMaxLength(80) - .HasColumnType("character varying(80)"); + .HasColumnType("nvarchar(80)"); b.Property("Number") - .HasColumnType("integer"); + .HasColumnType("int"); b.HasKey("Id"); @@ -218,28 +220,28 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Author") .IsRequired() .HasMaxLength(120) - .HasColumnType("character varying(120)"); + .HasColumnType("nvarchar(120)"); b.Property("CoverUrl") - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("IsPublished") - .HasColumnType("boolean"); + .HasColumnType("bit"); b.Property("Slug") .IsRequired() .HasMaxLength(120) - .HasColumnType("character varying(120)"); + .HasColumnType("nvarchar(120)"); b.Property("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("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("AudioUrl") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("AyahNumber") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("DurationSeconds") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("SortOrder") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("Summary") - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("SurahId") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("TafsirId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Title") .IsRequired() .HasMaxLength(300) - .HasColumnType("character varying(300)"); + .HasColumnType("nvarchar(300)"); b.HasKey("Id"); diff --git a/src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009174748_InitialCreate.cs b/src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009190049_InitialSqlServer.cs similarity index 68% rename from src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009174748_InitialCreate.cs rename to src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009190049_InitialSqlServer.cs index c2ab1b4..d868d89 100644 --- a/src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009174748_InitialCreate.cs +++ b/src/TafsirQuran.Infrastructure/Persistence/Migrations/20261009190049_InitialSqlServer.cs @@ -1,13 +1,12 @@ using System; using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable namespace TafsirQuran.Infrastructure.Persistence.Migrations { /// - public partial class InitialCreate : Migration + public partial class InitialSqlServer : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -16,11 +15,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations name: "Surahs", columns: table => new { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Number = table.Column(type: "integer", nullable: false), - NameAr = table.Column(type: "character varying(80)", maxLength: 80, nullable: false), - NameFa = table.Column(type: "character varying(80)", maxLength: 80, nullable: false) + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Number = table.Column(type: "int", nullable: false), + NameAr = table.Column(type: "nvarchar(80)", maxLength: 80, nullable: false), + NameFa = table.Column(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(type: "uuid", nullable: false), - Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Author = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - Slug = table.Column(type: "character varying(120)", maxLength: 120, nullable: false), - CoverUrl = table.Column(type: "text", nullable: true), - IsPublished = table.Column(type: "boolean", nullable: false) + Id = table.Column(type: "uniqueidentifier", nullable: false), + Title = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Author = table.Column(type: "nvarchar(120)", maxLength: 120, nullable: false), + Slug = table.Column(type: "nvarchar(120)", maxLength: 120, nullable: false), + CoverUrl = table.Column(type: "nvarchar(max)", nullable: true), + IsPublished = table.Column(type: "bit", nullable: false) }, constraints: table => { @@ -47,11 +46,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations name: "Users", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - PasswordHash = table.Column(type: "text", nullable: false), - DisplayName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + Id = table.Column(type: "uniqueidentifier", nullable: false), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: false), + DisplayName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false) }, constraints: table => { @@ -62,11 +61,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations name: "Ayahs", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - SurahId = table.Column(type: "integer", nullable: false), - Number = table.Column(type: "integer", nullable: false), - TextAr = table.Column(type: "text", nullable: false), - TextFa = table.Column(type: "text", nullable: true) + Id = table.Column(type: "uniqueidentifier", nullable: false), + SurahId = table.Column(type: "int", nullable: false), + Number = table.Column(type: "int", nullable: false), + TextAr = table.Column(type: "nvarchar(max)", nullable: false), + TextFa = table.Column(type: "nvarchar(max)", nullable: true) }, constraints: table => { @@ -83,15 +82,15 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations name: "Episodes", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - TafsirId = table.Column(type: "uuid", nullable: false), - SurahId = table.Column(type: "integer", nullable: false), - AyahNumber = table.Column(type: "integer", nullable: false), - Title = table.Column(type: "character varying(300)", maxLength: 300, nullable: false), - AudioUrl = table.Column(type: "text", nullable: false), - DurationSeconds = table.Column(type: "integer", nullable: false), - SortOrder = table.Column(type: "integer", nullable: false), - Summary = table.Column(type: "text", nullable: true) + Id = table.Column(type: "uniqueidentifier", nullable: false), + TafsirId = table.Column(type: "uniqueidentifier", nullable: false), + SurahId = table.Column(type: "int", nullable: false), + AyahNumber = table.Column(type: "int", nullable: false), + Title = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: false), + AudioUrl = table.Column(type: "nvarchar(max)", nullable: false), + DurationSeconds = table.Column(type: "int", nullable: false), + SortOrder = table.Column(type: "int", nullable: false), + Summary = table.Column(type: "nvarchar(max)", nullable: true) }, constraints: table => { @@ -114,12 +113,12 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations name: "Bookmarks", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: true), - DeviceId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - EpisodeId = table.Column(type: "uuid", nullable: false), - Note = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + Id = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "uniqueidentifier", nullable: true), + DeviceId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + EpisodeId = table.Column(type: "uniqueidentifier", nullable: false), + Note = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false) }, constraints: table => { @@ -141,11 +140,11 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations name: "Favorites", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: true), - DeviceId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - EpisodeId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + Id = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "uniqueidentifier", nullable: true), + DeviceId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + EpisodeId = table.Column(type: "uniqueidentifier", nullable: false), + CreatedAt = table.Column(type: "datetimeoffset", nullable: false) }, constraints: table => { @@ -167,12 +166,12 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations name: "ListeningProgresses", columns: table => new { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: true), - DeviceId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - EpisodeId = table.Column(type: "uuid", nullable: false), - PositionSeconds = table.Column(type: "double precision", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + Id = table.Column(type: "uniqueidentifier", nullable: false), + UserId = table.Column(type: "uniqueidentifier", nullable: true), + DeviceId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + EpisodeId = table.Column(type: "uniqueidentifier", nullable: false), + PositionSeconds = table.Column(type: "float", nullable: false), + UpdatedAt = table.Column(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", diff --git a/src/TafsirQuran.Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs b/src/TafsirQuran.Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs index e76e19e..ee659a0 100644 --- a/src/TafsirQuran.Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/src/TafsirQuran.Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -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("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetimeoffset"); b.Property("DisplayName") .IsRequired() .HasMaxLength(128) - .HasColumnType("character varying(128)"); + .HasColumnType("nvarchar(128)"); b.Property("Email") .IsRequired() .HasMaxLength(256) - .HasColumnType("character varying(256)"); + .HasColumnType("nvarchar(256)"); b.Property("PasswordHash") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -57,20 +57,20 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Number") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("SurahId") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("TextAr") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("TextFa") - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.HasKey("Id"); @@ -84,25 +84,25 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetimeoffset"); b.Property("DeviceId") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("nvarchar(64)"); b.Property("EpisodeId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Note") .HasMaxLength(500) - .HasColumnType("character varying(500)"); + .HasColumnType("nvarchar(500)"); b.Property("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("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetimeoffset"); b.Property("DeviceId") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("nvarchar(64)"); b.Property("EpisodeId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("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("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("DeviceId") .IsRequired() .HasMaxLength(64) - .HasColumnType("character varying(64)"); + .HasColumnType("nvarchar(64)"); b.Property("EpisodeId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("PositionSeconds") - .HasColumnType("double precision"); + .HasColumnType("float"); b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); + .HasColumnType("datetimeoffset"); b.Property("UserId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.HasKey("Id"); @@ -186,22 +188,22 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("integer"); + .HasColumnType("int"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("NameAr") .IsRequired() .HasMaxLength(80) - .HasColumnType("character varying(80)"); + .HasColumnType("nvarchar(80)"); b.Property("NameFa") .IsRequired() .HasMaxLength(80) - .HasColumnType("character varying(80)"); + .HasColumnType("nvarchar(80)"); b.Property("Number") - .HasColumnType("integer"); + .HasColumnType("int"); b.HasKey("Id"); @@ -215,28 +217,28 @@ namespace TafsirQuran.Infrastructure.Persistence.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Author") .IsRequired() .HasMaxLength(120) - .HasColumnType("character varying(120)"); + .HasColumnType("nvarchar(120)"); b.Property("CoverUrl") - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("IsPublished") - .HasColumnType("boolean"); + .HasColumnType("bit"); b.Property("Slug") .IsRequired() .HasMaxLength(120) - .HasColumnType("character varying(120)"); + .HasColumnType("nvarchar(120)"); b.Property("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("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("AudioUrl") .IsRequired() - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("AyahNumber") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("DurationSeconds") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("SortOrder") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("Summary") - .HasColumnType("text"); + .HasColumnType("nvarchar(max)"); b.Property("SurahId") - .HasColumnType("integer"); + .HasColumnType("int"); b.Property("TafsirId") - .HasColumnType("uuid"); + .HasColumnType("uniqueidentifier"); b.Property("Title") .IsRequired() .HasMaxLength(300) - .HasColumnType("character varying(300)"); + .HasColumnType("nvarchar(300)"); b.HasKey("Id"); diff --git a/src/TafsirQuran.Infrastructure/TafsirQuran.Infrastructure.csproj b/src/TafsirQuran.Infrastructure/TafsirQuran.Infrastructure.csproj index ff794ad..4158bc3 100644 --- a/src/TafsirQuran.Infrastructure/TafsirQuran.Infrastructure.csproj +++ b/src/TafsirQuran.Infrastructure/TafsirQuran.Infrastructure.csproj @@ -9,13 +9,13 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/TafsirQuran.Web/App.razor b/src/TafsirQuran.Web/App.razor new file mode 100644 index 0000000..7e55dbe --- /dev/null +++ b/src/TafsirQuran.Web/App.razor @@ -0,0 +1,14 @@ +@using TafsirQuran.UI +@using TafsirQuran.UI.Layout + + + + + + + + +

صفحه پیدا نشد.

+
+
+
diff --git a/src/TafsirQuran.Web/Components/Layout/MainLayout.razor b/src/TafsirQuran.Web/Components/Layout/MainLayout.razor deleted file mode 100644 index 96fbbe6..0000000 --- a/src/TafsirQuran.Web/Components/Layout/MainLayout.razor +++ /dev/null @@ -1,9 +0,0 @@ -@inherits LayoutComponentBase - -@Body - -
- An unhandled error has occurred. - Reload - 🗙 -
diff --git a/src/TafsirQuran.Web/Components/Layout/MainLayout.razor.css b/src/TafsirQuran.Web/Components/Layout/MainLayout.razor.css deleted file mode 100644 index 60cec92..0000000 --- a/src/TafsirQuran.Web/Components/Layout/MainLayout.razor.css +++ /dev/null @@ -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; - } diff --git a/src/TafsirQuran.Web/Components/Pages/Error.razor b/src/TafsirQuran.Web/Components/Pages/Error.razor deleted file mode 100644 index 576cc2d..0000000 --- a/src/TafsirQuran.Web/Components/Pages/Error.razor +++ /dev/null @@ -1,36 +0,0 @@ -@page "/Error" -@using System.Diagnostics - -Error - -

Error.

-

An error occurred while processing your request.

- -@if (ShowRequestId) -{ -

- Request ID: @RequestId -

-} - -

Development Mode

-

- Swapping to Development environment will display more detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

- -@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; -} diff --git a/src/TafsirQuran.Web/Program.cs b/src/TafsirQuran.Web/Program.cs index 1fa4fa9..19e9ea3 100644 --- a/src/TafsirQuran.Web/Program.cs +++ b/src/TafsirQuran.Web/Program.cs @@ -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"); +builder.RootComponents.Add("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() - .AddInteractiveServerRenderMode() - .AddAdditionalAssemblies(typeof(TafsirQuran.UI.Routes).Assembly); - -app.Run(); +await builder.Build().RunAsync(); diff --git a/src/TafsirQuran.Web/Properties/launchSettings.json b/src/TafsirQuran.Web/Properties/launchSettings.json index 420fe45..9aac440 100644 --- a/src/TafsirQuran.Web/Properties/launchSettings.json +++ b/src/TafsirQuran.Web/Properties/launchSettings.json @@ -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" } } } +} diff --git a/src/TafsirQuran.Web/TafsirQuran.Web.csproj b/src/TafsirQuran.Web/TafsirQuran.Web.csproj index f127c48..2b1b879 100644 --- a/src/TafsirQuran.Web/TafsirQuran.Web.csproj +++ b/src/TafsirQuran.Web/TafsirQuran.Web.csproj @@ -1,14 +1,24 @@ - + net9.0 enable enable + service-worker-assets.js + + + + + + + + + diff --git a/src/TafsirQuran.Web/Components/_Imports.razor b/src/TafsirQuran.Web/_Imports.razor similarity index 69% rename from src/TafsirQuran.Web/Components/_Imports.razor rename to src/TafsirQuran.Web/_Imports.razor index df82f4f..86d2b08 100644 --- a/src/TafsirQuran.Web/Components/_Imports.razor +++ b/src/TafsirQuran.Web/_Imports.razor @@ -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 diff --git a/src/TafsirQuran.Web/appsettings.Development.json b/src/TafsirQuran.Web/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/src/TafsirQuran.Web/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/src/TafsirQuran.Web/appsettings.json b/src/TafsirQuran.Web/appsettings.json deleted file mode 100644 index 734c29c..0000000 --- a/src/TafsirQuran.Web/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ApiBaseAddress": "http://localhost:5080/", - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/src/TafsirQuran.Web/wwwroot/appsettings.Development.json b/src/TafsirQuran.Web/wwwroot/appsettings.Development.json new file mode 100644 index 0000000..0222cd3 --- /dev/null +++ b/src/TafsirQuran.Web/wwwroot/appsettings.Development.json @@ -0,0 +1,3 @@ +{ + "ApiBaseAddress": "http://localhost:5080/" +} diff --git a/src/TafsirQuran.Web/wwwroot/appsettings.json b/src/TafsirQuran.Web/wwwroot/appsettings.json new file mode 100644 index 0000000..7f1699c --- /dev/null +++ b/src/TafsirQuran.Web/wwwroot/appsettings.json @@ -0,0 +1,3 @@ +{ + "ApiBaseAddress": "http://185.252.31.44:30821/" +} diff --git a/src/TafsirQuran.Web/Components/App.razor b/src/TafsirQuran.Web/wwwroot/index.html similarity index 62% rename from src/TafsirQuran.Web/Components/App.razor rename to src/TafsirQuran.Web/wwwroot/index.html index 48d4598..205a4ce 100644 --- a/src/TafsirQuran.Web/Components/App.razor +++ b/src/TafsirQuran.Web/wwwroot/index.html @@ -1,4 +1,4 @@ - + @@ -6,19 +6,22 @@ + تفسیر قرآن - - + + - - +
+
در حال بارگذاری...
+
+ + - diff --git a/src/TafsirQuran.Web/wwwroot/service-worker-register.js b/src/TafsirQuran.Web/wwwroot/service-worker-register.js deleted file mode 100644 index 0082ce2..0000000 --- a/src/TafsirQuran.Web/wwwroot/service-worker-register.js +++ /dev/null @@ -1,5 +0,0 @@ -if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('/service-worker.js').catch(() => {}); - }); -} diff --git a/src/TafsirQuran.Web/wwwroot/service-worker.js b/src/TafsirQuran.Web/wwwroot/service-worker.js index 0291dc9..c5c10ea 100644 --- a/src/TafsirQuran.Web/wwwroot/service-worker.js +++ b/src/TafsirQuran.Web/wwwroot/service-worker.js @@ -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', () => { }); diff --git a/src/TafsirQuran.Web/wwwroot/service-worker.published.js b/src/TafsirQuran.Web/wwwroot/service-worker.published.js new file mode 100644 index 0000000..d987473 --- /dev/null +++ b/src/TafsirQuran.Web/wwwroot/service-worker.published.js @@ -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; + }) + ); +});