Add Surah/Ayah browse with Gharaati Tafsir Noor crawl.
build-and-deploy / deploy (push) Failing after 11m0s
build-and-deploy / deploy (push) Failing after 11m0s
Replace episode bookmarks with surah/ayah bookmarks and ayah favorites; import ayah text, notes, messages, and official per-ayah audio from gharaati.ir.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TafsirQuran.Api.Auth;
|
||||
using TafsirQuran.Api.Mapping;
|
||||
using TafsirQuran.Domain.Entities;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
using TafsirQuran.Shared.Dtos;
|
||||
@@ -12,52 +11,106 @@ public static class BookmarkEndpoints
|
||||
{
|
||||
public static RouteGroupBuilder MapBookmarkEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/bookmarks");
|
||||
var surahGroup = app.MapGroup("/api/bookmarks/surahs");
|
||||
var ayahGroup = app.MapGroup("/api/bookmarks/ayahs");
|
||||
|
||||
group.MapGet("/", async (string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
surahGroup.MapGet("/", async (string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var items = await db.Bookmarks.AsNoTracking()
|
||||
.Include(b => b.Episode).ThenInclude(e => e.Surah)
|
||||
var items = await db.SurahBookmarks.AsNoTracking()
|
||||
.Include(b => b.Surah)
|
||||
.Where(b => b.DeviceId == deviceId || (userId.HasValue && b.UserId == userId))
|
||||
.OrderByDescending(b => b.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Results.Ok(items.Select(b => new BookmarkDto(
|
||||
b.Id, b.EpisodeId, b.Note, b.CreatedAt, EpisodeMapper.ToListItem(b.Episode))).ToList());
|
||||
return Results.Ok(items.Select(b => new SurahBookmarkDto(
|
||||
b.Id, b.TafsirId, b.SurahId, b.Surah.NameFa, b.Note, b.CreatedAt)).ToList());
|
||||
});
|
||||
|
||||
group.MapPost("/", async (ToggleEpisodeRequest request, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
surahGroup.MapPost("/", async (ToggleSurahBookmarkRequest request, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var exists = await db.Bookmarks.AnyAsync(b =>
|
||||
b.EpisodeId == request.EpisodeId &&
|
||||
var exists = await db.SurahBookmarks.AnyAsync(b =>
|
||||
b.TafsirId == request.TafsirId &&
|
||||
b.SurahId == request.SurahNumber &&
|
||||
(b.DeviceId == request.DeviceId || (userId.HasValue && b.UserId == userId)), ct);
|
||||
if (exists) return Results.NoContent();
|
||||
|
||||
db.Bookmarks.Add(new Bookmark
|
||||
db.SurahBookmarks.Add(new SurahBookmark
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = userId,
|
||||
EpisodeId = request.EpisodeId,
|
||||
TafsirId = request.TafsirId,
|
||||
SurahId = request.SurahNumber,
|
||||
Note = request.Note
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Created($"/api/bookmarks/{request.EpisodeId}", null);
|
||||
return Results.Created($"/api/bookmarks/surahs/{request.SurahNumber}", null);
|
||||
});
|
||||
|
||||
group.MapDelete("/{episodeId:guid}", async (Guid episodeId, string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
surahGroup.MapDelete("/{surahNumber:int}", async (
|
||||
int surahNumber, Guid tafsirId, string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var items = await db.Bookmarks
|
||||
.Where(b => b.EpisodeId == episodeId &&
|
||||
var items = await db.SurahBookmarks
|
||||
.Where(b => b.TafsirId == tafsirId && b.SurahId == surahNumber &&
|
||||
(b.DeviceId == deviceId || (userId.HasValue && b.UserId == userId)))
|
||||
.ToListAsync(ct);
|
||||
db.Bookmarks.RemoveRange(items);
|
||||
db.SurahBookmarks.RemoveRange(items);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
return group;
|
||||
ayahGroup.MapGet("/", async (string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var items = await db.AyahBookmarks.AsNoTracking()
|
||||
.Include(b => b.Surah)
|
||||
.Include(b => b.Tafsir)
|
||||
.Where(b => b.DeviceId == deviceId || (userId.HasValue && b.UserId == userId))
|
||||
.OrderByDescending(b => b.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Results.Ok(items.Select(b => new AyahBookmarkDto(
|
||||
b.Id, b.TafsirId, b.Tafsir.Slug, b.SurahId, b.Surah.NameFa, b.AyahNumber, b.Note, b.CreatedAt)).ToList());
|
||||
});
|
||||
|
||||
ayahGroup.MapPost("/", async (ToggleAyahRequest request, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var exists = await db.AyahBookmarks.AnyAsync(b =>
|
||||
b.TafsirId == request.TafsirId &&
|
||||
b.SurahId == request.SurahNumber &&
|
||||
b.AyahNumber == request.AyahNumber &&
|
||||
(b.DeviceId == request.DeviceId || (userId.HasValue && b.UserId == userId)), ct);
|
||||
if (exists) return Results.NoContent();
|
||||
|
||||
db.AyahBookmarks.Add(new AyahBookmark
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = userId,
|
||||
TafsirId = request.TafsirId,
|
||||
SurahId = request.SurahNumber,
|
||||
AyahNumber = request.AyahNumber,
|
||||
Note = request.Note
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Created($"/api/bookmarks/ayahs/{request.SurahNumber}/{request.AyahNumber}", null);
|
||||
});
|
||||
|
||||
ayahGroup.MapDelete("/{surahNumber:int}/{ayahNumber:int}", async (
|
||||
int surahNumber, int ayahNumber, Guid tafsirId, string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var items = await db.AyahBookmarks
|
||||
.Where(b => b.TafsirId == tafsirId && b.SurahId == surahNumber && b.AyahNumber == ayahNumber &&
|
||||
(b.DeviceId == deviceId || (userId.HasValue && b.UserId == userId)))
|
||||
.ToListAsync(ct);
|
||||
db.AyahBookmarks.RemoveRange(items);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
return surahGroup;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TafsirQuran.Api.Auth;
|
||||
using TafsirQuran.Api.Mapping;
|
||||
using TafsirQuran.Domain.Entities;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
using TafsirQuran.Shared.Dtos;
|
||||
@@ -12,47 +11,53 @@ public static class FavoriteEndpoints
|
||||
{
|
||||
public static RouteGroupBuilder MapFavoriteEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/favorites");
|
||||
var group = app.MapGroup("/api/favorites/ayahs");
|
||||
|
||||
group.MapGet("/", async (string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var items = await db.Favorites.AsNoTracking()
|
||||
.Include(f => f.Episode).ThenInclude(e => e.Surah)
|
||||
var items = await db.AyahFavorites.AsNoTracking()
|
||||
.Include(f => f.Surah)
|
||||
.Include(f => f.Tafsir)
|
||||
.Where(f => f.DeviceId == deviceId || (userId.HasValue && f.UserId == userId))
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Results.Ok(items.Select(f => new FavoriteDto(
|
||||
f.Id, f.EpisodeId, f.CreatedAt, EpisodeMapper.ToListItem(f.Episode))).ToList());
|
||||
return Results.Ok(items.Select(f => new AyahFavoriteDto(
|
||||
f.Id, f.TafsirId, f.Tafsir.Slug, f.SurahId, f.Surah.NameFa, f.AyahNumber, f.CreatedAt)).ToList());
|
||||
});
|
||||
|
||||
group.MapPost("/", async (ToggleEpisodeRequest request, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
group.MapPost("/", async (ToggleAyahRequest request, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var exists = await db.Favorites.AnyAsync(f =>
|
||||
f.EpisodeId == request.EpisodeId &&
|
||||
var exists = await db.AyahFavorites.AnyAsync(f =>
|
||||
f.TafsirId == request.TafsirId &&
|
||||
f.SurahId == request.SurahNumber &&
|
||||
f.AyahNumber == request.AyahNumber &&
|
||||
(f.DeviceId == request.DeviceId || (userId.HasValue && f.UserId == userId)), ct);
|
||||
if (exists) return Results.NoContent();
|
||||
|
||||
db.Favorites.Add(new Favorite
|
||||
db.AyahFavorites.Add(new AyahFavorite
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = userId,
|
||||
EpisodeId = request.EpisodeId
|
||||
TafsirId = request.TafsirId,
|
||||
SurahId = request.SurahNumber,
|
||||
AyahNumber = request.AyahNumber
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Created($"/api/favorites/{request.EpisodeId}", null);
|
||||
return Results.Created($"/api/favorites/ayahs/{request.SurahNumber}/{request.AyahNumber}", null);
|
||||
});
|
||||
|
||||
group.MapDelete("/{episodeId:guid}", async (Guid episodeId, string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
group.MapDelete("/{surahNumber:int}/{ayahNumber:int}", async (
|
||||
int surahNumber, int ayahNumber, Guid tafsirId, string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var items = await db.Favorites
|
||||
.Where(f => f.EpisodeId == episodeId &&
|
||||
var items = await db.AyahFavorites
|
||||
.Where(f => f.TafsirId == tafsirId && f.SurahId == surahNumber && f.AyahNumber == ayahNumber &&
|
||||
(f.DeviceId == deviceId || (userId.HasValue && f.UserId == userId)))
|
||||
.ToListAsync(ct);
|
||||
db.Favorites.RemoveRange(items);
|
||||
db.AyahFavorites.RemoveRange(items);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TafsirQuran.Api.Auth;
|
||||
using TafsirQuran.Api.Mapping;
|
||||
using TafsirQuran.Domain.Entities;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
using TafsirQuran.Shared.Dtos;
|
||||
@@ -17,22 +16,22 @@ public static class ProgressEndpoints
|
||||
group.MapGet("/", async (string deviceId, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = JwtTokenService.GetUserId(user);
|
||||
var query = db.ListeningProgresses.AsNoTracking().Include(p => p.Episode).ThenInclude(e => e.Surah).AsQueryable();
|
||||
var query = db.ListeningProgresses.AsNoTracking().Include(p => p.Surah).AsQueryable();
|
||||
|
||||
ListeningProgress? progress = null;
|
||||
if (userId.HasValue)
|
||||
{
|
||||
progress = await query.Where(p => p.UserId == userId).OrderByDescending(p => p.UpdatedAt).FirstOrDefaultAsync(ct);
|
||||
}
|
||||
progress ??= await query.Where(p => p.DeviceId == deviceId).OrderByDescending(p => p.UpdatedAt).FirstOrDefaultAsync(ct);
|
||||
|
||||
if (progress is null) return Results.NoContent();
|
||||
|
||||
return Results.Ok(new ProgressDto(
|
||||
progress.EpisodeId,
|
||||
progress.TafsirId,
|
||||
progress.SurahId,
|
||||
progress.AyahNumber,
|
||||
progress.PositionSeconds,
|
||||
progress.UpdatedAt,
|
||||
EpisodeMapper.ToListItem(progress.Episode)));
|
||||
progress.Surah.NameFa));
|
||||
});
|
||||
|
||||
group.MapPut("/", async (UpsertProgressRequest request, ClaimsPrincipal user, AppDbContext db, CancellationToken ct) =>
|
||||
@@ -52,14 +51,18 @@ public static class ProgressEndpoints
|
||||
{
|
||||
DeviceId = request.DeviceId,
|
||||
UserId = userId,
|
||||
EpisodeId = request.EpisodeId,
|
||||
TafsirId = request.TafsirId,
|
||||
SurahId = request.SurahNumber,
|
||||
AyahNumber = request.AyahNumber,
|
||||
PositionSeconds = request.PositionSeconds
|
||||
};
|
||||
db.ListeningProgresses.Add(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.EpisodeId = request.EpisodeId;
|
||||
existing.TafsirId = request.TafsirId;
|
||||
existing.SurahId = request.SurahNumber;
|
||||
existing.AyahNumber = request.AyahNumber;
|
||||
existing.PositionSeconds = request.PositionSeconds;
|
||||
existing.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
existing.UserId ??= userId;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TafsirQuran.Api.Mapping;
|
||||
using TafsirQuran.Infrastructure.Persistence;
|
||||
using TafsirQuran.Shared.Dtos;
|
||||
|
||||
@@ -22,67 +21,178 @@ public static class TafsirEndpoints
|
||||
return Results.Ok(items);
|
||||
});
|
||||
|
||||
group.MapGet("/tafsirs/{slug}/episodes", async (string slug, AppDbContext db, CancellationToken ct) =>
|
||||
group.MapGet("/surahs", async (AppDbContext db, string? deviceId, Guid? tafsirId, CancellationToken ct) =>
|
||||
{
|
||||
var tid = tafsirId ?? DbSeeder.GhatrehChekaniId;
|
||||
var bookmarked = new HashSet<int>();
|
||||
if (!string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
bookmarked = (await db.SurahBookmarks.AsNoTracking()
|
||||
.Where(b => b.TafsirId == tid && b.DeviceId == deviceId)
|
||||
.Select(b => b.SurahId)
|
||||
.ToListAsync(ct))
|
||||
.ToHashSet();
|
||||
}
|
||||
|
||||
var ayahCounts = await db.Ayahs.AsNoTracking()
|
||||
.GroupBy(a => a.SurahId)
|
||||
.Select(g => new { SurahId = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.SurahId, x => x.Count, ct);
|
||||
|
||||
// Fallback: content rows if ayahs not yet imported for a surah
|
||||
var contentCounts = await db.TafsirAyahContents.AsNoTracking()
|
||||
.Where(c => c.TafsirId == tid)
|
||||
.GroupBy(c => c.SurahId)
|
||||
.Select(g => new { SurahId = g.Key, Count = g.Count() })
|
||||
.ToDictionaryAsync(x => x.SurahId, x => x.Count, ct);
|
||||
|
||||
var surahs = await db.Surahs.AsNoTracking().OrderBy(s => s.Number).ToListAsync(ct);
|
||||
var result = surahs.Select(s =>
|
||||
{
|
||||
var count = ayahCounts.GetValueOrDefault(s.Id);
|
||||
if (count == 0) count = contentCounts.GetValueOrDefault(s.Id);
|
||||
return new SurahListItemDto(s.Number, s.NameAr, s.NameFa, count, bookmarked.Contains(s.Id));
|
||||
}).ToList();
|
||||
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
group.MapGet("/tafsirs/{slug}/surahs/{surahNumber:int}/ayahs", async (
|
||||
string slug, int surahNumber, string? deviceId, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var tafsir = await db.Tafsirs.AsNoTracking().FirstOrDefaultAsync(t => t.Slug == slug && t.IsPublished, ct);
|
||||
if (tafsir is null) return Results.NotFound();
|
||||
|
||||
var episodes = await db.Episodes
|
||||
.AsNoTracking()
|
||||
.Include(e => e.Surah)
|
||||
.Where(e => e.TafsirId == tafsir.Id)
|
||||
.OrderBy(e => e.SortOrder)
|
||||
.ToListAsync(ct);
|
||||
var surah = await db.Surahs.AsNoTracking().FirstOrDefaultAsync(s => s.Number == surahNumber, ct);
|
||||
if (surah is null) return Results.NotFound();
|
||||
|
||||
return Results.Ok(episodes.Select(e => EpisodeMapper.ToListItem(e)).ToList());
|
||||
});
|
||||
|
||||
group.MapGet("/episodes/{id:guid}", async (Guid id, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var episode = await db.Episodes
|
||||
.AsNoTracking()
|
||||
.Include(e => e.Surah)
|
||||
.Include(e => e.Tafsir)
|
||||
.FirstOrDefaultAsync(e => e.Id == id, ct);
|
||||
if (episode is null) return Results.NotFound();
|
||||
|
||||
var ayah = await db.Ayahs.AsNoTracking()
|
||||
.FirstOrDefaultAsync(a => a.SurahId == episode.SurahId && a.Number == episode.AyahNumber, ct);
|
||||
|
||||
var detail = EpisodeMapper.ToDetail(episode, ayah) with
|
||||
var bookmarks = new HashSet<int>();
|
||||
var favorites = new HashSet<int>();
|
||||
if (!string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
AudioUrl = $"/api/audio/{episode.Id}"
|
||||
};
|
||||
return Results.Ok(detail);
|
||||
});
|
||||
|
||||
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 audioRoot = config["AudioRoot"] ?? "/data/audio";
|
||||
var relative = episode.AudioUrl.TrimStart('/').Replace('\\', '/');
|
||||
|
||||
// Prefer PVC mount; fallback to wwwroot for local sample files
|
||||
var candidates = new[]
|
||||
{
|
||||
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);
|
||||
bookmarks = (await db.AyahBookmarks.AsNoTracking()
|
||||
.Where(b => b.TafsirId == tafsir.Id && b.SurahId == surah.Id && b.DeviceId == deviceId)
|
||||
.Select(b => b.AyahNumber).ToListAsync(ct)).ToHashSet();
|
||||
favorites = (await db.AyahFavorites.AsNoTracking()
|
||||
.Where(f => f.TafsirId == tafsir.Id && f.SurahId == surah.Id && f.DeviceId == deviceId)
|
||||
.Select(f => f.AyahNumber).ToListAsync(ct)).ToHashSet();
|
||||
}
|
||||
|
||||
var silence = Path.Combine(env.ContentRootPath, "wwwroot", "audio", "silence.mp3");
|
||||
if (File.Exists(silence))
|
||||
return Results.File(silence, "audio/mpeg", enableRangeProcessing: true);
|
||||
var contents = await db.TafsirAyahContents.AsNoTracking()
|
||||
.Where(c => c.TafsirId == tafsir.Id && c.SurahId == surah.Id)
|
||||
.ToDictionaryAsync(c => c.AyahNumber, ct);
|
||||
|
||||
return Results.NotFound();
|
||||
var ayahs = await db.Ayahs.AsNoTracking()
|
||||
.Where(a => a.SurahId == surah.Id)
|
||||
.OrderBy(a => a.Number)
|
||||
.ToListAsync(ct);
|
||||
|
||||
if (ayahs.Count == 0 && contents.Count > 0)
|
||||
{
|
||||
ayahs = contents.Keys.OrderBy(k => k).Select(n => new Domain.Entities.Ayah
|
||||
{
|
||||
SurahId = surah.Id,
|
||||
Number = n,
|
||||
TextFa = null
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
var list = ayahs.Select(a =>
|
||||
{
|
||||
contents.TryGetValue(a.Number, out var c);
|
||||
var preview = a.TextFa;
|
||||
if (!string.IsNullOrWhiteSpace(preview) && preview.Length > 80)
|
||||
preview = preview[..80] + "…";
|
||||
return new AyahListItemDto(
|
||||
surah.Number,
|
||||
surah.NameFa,
|
||||
a.Number,
|
||||
preview,
|
||||
!string.IsNullOrWhiteSpace(c?.AudioPath),
|
||||
bookmarks.Contains(a.Number),
|
||||
favorites.Contains(a.Number));
|
||||
}).ToList();
|
||||
|
||||
return Results.Ok(list);
|
||||
});
|
||||
|
||||
group.MapGet("/tafsirs/{slug}/surahs/{surahNumber:int}/ayahs/{ayahNumber:int}", async (
|
||||
string slug, int surahNumber, int ayahNumber, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var tafsir = await db.Tafsirs.AsNoTracking().FirstOrDefaultAsync(t => t.Slug == slug && t.IsPublished, ct);
|
||||
if (tafsir is null) return Results.NotFound();
|
||||
|
||||
var surah = await db.Surahs.AsNoTracking().FirstOrDefaultAsync(s => s.Number == surahNumber, ct);
|
||||
if (surah is null) return Results.NotFound();
|
||||
|
||||
var ayah = await db.Ayahs.AsNoTracking()
|
||||
.FirstOrDefaultAsync(a => a.SurahId == surah.Id && a.Number == ayahNumber, ct);
|
||||
var content = await db.TafsirAyahContents.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.TafsirId == tafsir.Id && c.SurahId == surah.Id && c.AyahNumber == ayahNumber, ct);
|
||||
|
||||
if (ayah is null && content is null) return Results.NotFound();
|
||||
|
||||
var numbers = await db.Ayahs.AsNoTracking()
|
||||
.Where(a => a.SurahId == surah.Id)
|
||||
.Select(a => a.Number)
|
||||
.OrderBy(n => n)
|
||||
.ToListAsync(ct);
|
||||
if (numbers.Count == 0)
|
||||
{
|
||||
numbers = await db.TafsirAyahContents.AsNoTracking()
|
||||
.Where(c => c.TafsirId == tafsir.Id && c.SurahId == surah.Id)
|
||||
.Select(c => c.AyahNumber)
|
||||
.OrderBy(n => n)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
int? prev = null, next = null;
|
||||
var idx = numbers.IndexOf(ayahNumber);
|
||||
if (idx > 0) prev = numbers[idx - 1];
|
||||
if (idx >= 0 && idx < numbers.Count - 1) next = numbers[idx + 1];
|
||||
|
||||
int? prevSurah = surahNumber > 1 ? surahNumber - 1 : null;
|
||||
int? nextSurah = surahNumber < 114 ? surahNumber + 1 : null;
|
||||
|
||||
string? audioUrl = null;
|
||||
if (!string.IsNullOrWhiteSpace(content?.AudioPath))
|
||||
audioUrl = $"/api/audio/ayah/{surahNumber}/{ayahNumber}?tafsirId={tafsir.Id}";
|
||||
|
||||
return Results.Ok(new AyahDetailDto(
|
||||
tafsir.Id,
|
||||
tafsir.Slug,
|
||||
surah.Number,
|
||||
surah.NameFa,
|
||||
surah.NameAr,
|
||||
ayahNumber,
|
||||
ayah?.TextAr ?? "",
|
||||
ayah?.TextFa,
|
||||
content?.NotesHtml,
|
||||
content?.MessagesHtml,
|
||||
content?.VideoEmbedUrl,
|
||||
audioUrl,
|
||||
prev,
|
||||
next,
|
||||
prevSurah,
|
||||
nextSurah));
|
||||
});
|
||||
|
||||
group.MapGet("/audio/ayah/{surahNumber:int}/{ayahNumber:int}", async (
|
||||
int surahNumber, int ayahNumber, Guid? tafsirId, AppDbContext db, IConfiguration config, CancellationToken ct) =>
|
||||
{
|
||||
var tid = tafsirId ?? DbSeeder.GhatrehChekaniId;
|
||||
var content = await db.TafsirAyahContents.AsNoTracking()
|
||||
.FirstOrDefaultAsync(c => c.TafsirId == tid && c.SurahId == surahNumber && c.AyahNumber == ayahNumber, ct);
|
||||
if (content is null || string.IsNullOrWhiteSpace(content.AudioPath))
|
||||
return Results.NotFound();
|
||||
|
||||
var audioRoot = config["AudioRoot"] ?? "/data/audio";
|
||||
var relative = content.AudioPath.TrimStart('/').Replace('\\', '/');
|
||||
var path = Path.Combine(audioRoot, relative);
|
||||
if (!File.Exists(path))
|
||||
return Results.NotFound();
|
||||
|
||||
return Results.File(path, "audio/mpeg", enableRangeProcessing: true);
|
||||
});
|
||||
|
||||
return group;
|
||||
|
||||
Reference in New Issue
Block a user