Remove Tajik: TajikGanjoor site, API, services, export, transliteration
- delete TajikGanjoor project (removed from solution) - drop Tajik endpoints/services/DTOs/entities; migration DiwanRemoveTajik drops their tables - remove Tajik admin buttons/handlers and appsettings sections - README is now Diwan's; upstream README kept in docs/GANJOOR-README.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
62
DIWAN.md
@ -1,62 +0,0 @@
|
|||||||
# دیوان · Diwan site
|
|
||||||
|
|
||||||
This is a fork of [GanjoorService](https://github.com/ganjoor/GanjoorService) (GPL-3.0), the software behind ganjoor.net, adapted to serve **classical Urdu poetry and prose** from [diwan-data](https://github.com/anas-rashid/diwan-data).
|
|
||||||
|
|
||||||
## Changes from upstream
|
|
||||||
|
|
||||||
- **Branding:** "گنجور" becomes "دیوان" throughout the site. Upstream is credited in the footer.
|
|
||||||
- **Urdu basics:** pages are `lang="ur"`. The home page, footer and century groups (Hijri centuries, e.g. "تیرہویں صدی ہجری") are in Urdu. Deeper pages, such as admin and account pages, are still Persian.
|
|
||||||
- **Fonts:** Noto Nastaliq Urdu by default, with a **نستعلیق / نسخ** switch (Noto Naskh Arabic) at the bottom left. The choice is remembered per browser.
|
|
||||||
- **Footer:** links to Ganjoor-only services (Hafez divination, music index, etc.) are removed. Links to the Wikisource source, the data and the code are added.
|
|
||||||
- **Linux/Docker:** `Dockerfile` + `docker-compose.yml` (SQL Server 2022, API, site, Caddy for HTTPS).
|
|
||||||
- **Config fixes so env vars work:** the four places that read `appsettings.json` directly now also read environment variables. The JWT issuer follows `RSecurityBackend:ApplicationName` instead of the hard-coded "Ganjoor". `deploy/entrypoint.sh` copies the settings that RSecurityBackend reads only from `appsettings.json` (connection string, secret, app name, admin email) into the file at container start.
|
|
||||||
- **Links:** `ganjoor.net` links to the site's own pages are now relative. Links to Ganjoor's other services (blog, audio, etc.) are left as they are.
|
|
||||||
- **Locale:** `ur-PK`.
|
|
||||||
|
|
||||||
## Deploy (Ubuntu/Debian x86-64, e.g. Vultr)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# 1. Docker
|
|
||||||
curl -fsSL https://get.docker.com | sh
|
|
||||||
|
|
||||||
# 2. Code + config
|
|
||||||
git clone https://github.com/anas-rashid/diwan.git && cd diwan
|
|
||||||
cp .env.example .env && nano .env # domains, passwords, admin email
|
|
||||||
|
|
||||||
# 3. DNS: point SITE_DOMAIN and API_DOMAIN (A records) at the server, then:
|
|
||||||
docker compose up -d --build # first build takes a few minutes
|
|
||||||
docker compose logs -f api # wait for "Application started"
|
|
||||||
```
|
|
||||||
|
|
||||||
SQL Server needs about 2 GB of RAM. Use a plan with at least 4 GB in total.
|
|
||||||
|
|
||||||
### Load the data
|
|
||||||
|
|
||||||
1. Open `https://SITE_DOMAIN/login` and sign in with `ADMIN_EMAIL` and the password **`Test!123`**. The first login creates the admin account with that fixed password (RSecurityBackend's default; upstream's guide is wrong about this). **Change it right away** in the user panel.
|
|
||||||
2. On the import page that opens (or **Admin → مالی و سایت → درونریزی دادهٔ عمومی**), choose **Internet URL** and enter:
|
|
||||||
```
|
|
||||||
https://cdn.jsdelivr.net/gh/anas-rashid/diwan-data@main/
|
|
||||||
```
|
|
||||||
3. The import runs in the background (about 11k poems). Century groups are rebuilt automatically when it finishes.
|
|
||||||
|
|
||||||
Re-running the import adds new poems and leaves existing ones untouched, so it can be repeated after diwan-data's daily sync.
|
|
||||||
|
|
||||||
### Update
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git pull && docker compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build locally (macOS/Linux)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd RMuseum # its global.json pins SDK 10.0.302; newer SDKs fail on some upstream Razor views
|
|
||||||
dotnet build RMuseum.csproj -p:EnableWindowsTargeting=true
|
|
||||||
dotnet build ../GanjooRazor/GanjooRazor.csproj -p:EnableWindowsTargeting=true
|
|
||||||
```
|
|
||||||
|
|
||||||
Running it needs SQL Server, so use the Docker setup above. SQL Server's image is x86-64 only.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
GPL-3.0, same as upstream (see `LICENSE`). Data: see [diwan-data](https://github.com/anas-rashid/diwan-data).
|
|
||||||
@ -228,14 +228,11 @@
|
|||||||
<a role="button" onclick="defaultHtmlText()" class="up-btn up-btn--ghost up-btn--sm" title="چینش پیشفرض ابیات">چینش پیشفرض ابیات</a>
|
<a role="button" onclick="defaultHtmlText()" class="up-btn up-btn--ghost up-btn--sm" title="چینش پیشفرض ابیات">چینش پیشفرض ابیات</a>
|
||||||
<a role="button" onclick="cleanCache(@Model.PageInformation.Id)" class="up-btn up-btn--ghost up-btn--sm" title="حذف کش صفحه">حذف کش صفحه</a>
|
<a role="button" onclick="cleanCache(@Model.PageInformation.Id)" class="up-btn up-btn--ghost up-btn--sm" title="حذف کش صفحه">حذف کش صفحه</a>
|
||||||
<a role="button" onclick="rebuildSitemap()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی نقشهٔ سایت">بازسازی نقشهٔ سایت</a>
|
<a role="button" onclick="rebuildSitemap()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی نقشهٔ سایت">بازسازی نقشهٔ سایت</a>
|
||||||
<a role="button" onclick="rebuildTajikSitemap()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی نقشهٔ سایت">بازسازی نقشهٔ سایت تاجیکی</a>
|
|
||||||
<a role="button" onclick="refillCoupletCounts()" class="up-btn up-btn--ghost up-btn--sm">بازشماری تعداد ابیات قطعات</a>
|
<a role="button" onclick="refillCoupletCounts()" class="up-btn up-btn--ghost up-btn--sm">بازشماری تعداد ابیات قطعات</a>
|
||||||
<a role="button" onclick="rebuildStats()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی صفحات آمار و اوزان عروضی">بازسازی صفحات آمار و اوزان عروضی</a>
|
<a role="button" onclick="rebuildStats()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی صفحات آمار و اوزان عروضی">بازسازی صفحات آمار و اوزان عروضی</a>
|
||||||
<a role="button" onclick="rebuildRelatedPages()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی صفحات استقبالها">بازسازی صفحات استقبالها</a>
|
<a role="button" onclick="rebuildRelatedPages()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی صفحات استقبالها">بازسازی صفحات استقبالها</a>
|
||||||
<a role="button" onclick="rebuildDigitalSourcesStats()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی آمار منابع دیجیتال">بازسازی آمار منابع دیجیتال</a>
|
<a role="button" onclick="rebuildDigitalSourcesStats()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی آمار منابع دیجیتال">بازسازی آمار منابع دیجیتال</a>
|
||||||
<a role="button" onclick="regeneratePublicData()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی و ارسال دادهٔ عمومی دیوان">بازسازی دادهٔ عمومی</a>
|
<a role="button" onclick="regeneratePublicData()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی و ارسال دادهٔ عمومی دیوان">بازسازی دادهٔ عمومی</a>
|
||||||
<a role="button" onclick="regenerateTajikPublicData()" class="up-btn up-btn--ghost up-btn--sm" title="بازسازی و ارسال دادهٔ عمومی تاجیکی">بازسازی دادهٔ عمومی تاجیکی</a>
|
|
||||||
<a role="button" onclick="transilerate()" class="up-btn up-btn--ghost up-btn--sm" title="برگردان تاجیکی">برگردان تاجیکی</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (Model.PageInformation.PoetOrCat != null)
|
@if (Model.PageInformation.PoetOrCat != null)
|
||||||
|
|||||||
@ -303,23 +303,6 @@ namespace GanjooRazor.Areas.Admin.Pages
|
|||||||
return new OkObjectResult(false);
|
return new OkObjectResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> OnPostRebuildTajikSitemapAsync()
|
|
||||||
{
|
|
||||||
using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
|
|
||||||
{
|
|
||||||
if (await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
|
|
||||||
{
|
|
||||||
HttpResponseMessage response = await secureClient.PostAsync($"{APIRoot.Url}/api/ganjoor/sitemap/tajik", null);
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return BadRequest(JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync()));
|
|
||||||
}
|
|
||||||
return new OkObjectResult(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new OkObjectResult(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<IActionResult> OnPostRebuildStatsAsync()
|
public async Task<IActionResult> OnPostRebuildStatsAsync()
|
||||||
{
|
{
|
||||||
@ -360,27 +343,6 @@ namespace GanjooRazor.Areas.Admin.Pages
|
|||||||
return new OkObjectResult(false);
|
return new OkObjectResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// (re)generate the separate Tajik (Cyrillic) public data export — same background-job
|
|
||||||
/// pattern as the main one, different repo
|
|
||||||
/// </summary>
|
|
||||||
public async Task<IActionResult> OnPostRegenerateTajikPublicDataAsync()
|
|
||||||
{
|
|
||||||
using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
|
|
||||||
{
|
|
||||||
if (await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
|
|
||||||
{
|
|
||||||
HttpResponseMessage response = await secureClient.PostAsync($"{APIRoot.Url}/api/ganjoor/tajik/publicdata/batchexport", null);
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return BadRequest(JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync()));
|
|
||||||
}
|
|
||||||
return new OkObjectResult(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new OkObjectResult(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<IActionResult> OnPostRefillSectionsCoupletCountsAsync()
|
public async Task<IActionResult> OnPostRefillSectionsCoupletCountsAsync()
|
||||||
{
|
{
|
||||||
@ -436,23 +398,6 @@ namespace GanjooRazor.Areas.Admin.Pages
|
|||||||
return new OkObjectResult(false);
|
return new OkObjectResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> OnPostTransilerateAsync()
|
|
||||||
{
|
|
||||||
using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
|
|
||||||
{
|
|
||||||
if (await GanjoorSessionChecker.PrepareClient(secureClient, Request, Response))
|
|
||||||
{
|
|
||||||
HttpResponseMessage response = await secureClient.PutAsync($"{APIRoot.Url}/api/ganjoor/transilerate", null);
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return BadRequest(JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync()));
|
|
||||||
}
|
|
||||||
return new OkObjectResult(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new OkObjectResult(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -158,25 +158,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rebuildTajikSitemap() {
|
|
||||||
var ok = await upConfirm('آیا از بازسازی نقشهٔ سایت تاجیکی اطمینان دارید؟');
|
|
||||||
if (!ok) return;
|
|
||||||
|
|
||||||
var url = '?handler=RebuildTajikSitemap';
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
type: "POST",
|
|
||||||
url: url,
|
|
||||||
error: function () {
|
|
||||||
upToast('خطا رخ داد.', 'error');
|
|
||||||
},
|
|
||||||
success: function () {
|
|
||||||
upToast('فرایند بازسازی نقشهٔ سایت تاجیکی شروع شد.', 'success');
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function rebuildStats() {
|
async function rebuildStats() {
|
||||||
@ -217,25 +198,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function regenerateTajikPublicData() {
|
|
||||||
var ok = await upConfirm('آیا از بازسازی و ارسال دادهٔ عمومی تاجیکی (مخزن گیت جداگانه) اطمینان دارید؟');
|
|
||||||
if (!ok) return;
|
|
||||||
|
|
||||||
var url = '?handler=RegenerateTajikPublicData';
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
type: "POST",
|
|
||||||
url: url,
|
|
||||||
error: function () {
|
|
||||||
upToast('خطا رخ داد.', 'error');
|
|
||||||
},
|
|
||||||
success: function () {
|
|
||||||
upToast('فرایند بازسازی دادهٔ عمومی تاجیکی در پسزمینه شروع شد.', 'success');
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function refillCoupletCounts() {
|
async function refillCoupletCounts() {
|
||||||
@ -297,25 +259,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function transilerate() {
|
|
||||||
var ok = await upConfirm('آیا از شروع برگردان تاجیکی اطمینان دارید؟');
|
|
||||||
if (!ok) return;
|
|
||||||
|
|
||||||
var url = '?handler=Transilerate';
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
type: "POST",
|
|
||||||
url: url,
|
|
||||||
error: function () {
|
|
||||||
upToast('خطا رخ داد.', 'error');
|
|
||||||
},
|
|
||||||
success: function () {
|
|
||||||
upToast('فرایند شروع شد.', 'success');
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rebuildRebuildWordCounts(poetId) {
|
async function rebuildRebuildWordCounts(poetId) {
|
||||||
var onlyCurrentPoet = await upConfirm('فقط شاعر جاری؟', { danger: false });
|
var onlyCurrentPoet = await upConfirm('فقط شاعر جاری؟', { danger: false });
|
||||||
if (!onlyCurrentPoet) {
|
if (!onlyCurrentPoet) {
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 18.8.12009.203 stable
|
VisualStudioVersion = 18.8.12009.203
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RMuseum", "RMuseum\RMuseum.csproj", "{113EF3DE-E8A9-40EF-9DA0-8DFABB3CDD68}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RMuseum", "RMuseum\RMuseum.csproj", "{113EF3DE-E8A9-40EF-9DA0-8DFABB3CDD68}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GanjooRazor", "GanjooRazor\GanjooRazor.csproj", "{48F9CDFA-625F-4743-BF21-BD556F6F33E8}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GanjooRazor", "GanjooRazor\GanjooRazor.csproj", "{48F9CDFA-625F-4743-BF21-BD556F6F33E8}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TajikGanjoor", "TajikGanjoor\TajikGanjoor.csproj", "{1C51EB06-5190-4646-84C7-5EE712C86F57}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -23,10 +21,6 @@ Global
|
|||||||
{48F9CDFA-625F-4743-BF21-BD556F6F33E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{48F9CDFA-625F-4743-BF21-BD556F6F33E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{48F9CDFA-625F-4743-BF21-BD556F6F33E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{48F9CDFA-625F-4743-BF21-BD556F6F33E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{48F9CDFA-625F-4743-BF21-BD556F6F33E8}.Release|Any CPU.Build.0 = Release|Any CPU
|
{48F9CDFA-625F-4743-BF21-BD556F6F33E8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{1C51EB06-5190-4646-84C7-5EE712C86F57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{1C51EB06-5190-4646-84C7-5EE712C86F57}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{1C51EB06-5190-4646-84C7-5EE712C86F57}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{1C51EB06-5190-4646-84C7-5EE712C86F57}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
78
README.md
@ -1,38 +1,64 @@
|
|||||||
> **This is Diwan (دیوان)**, a fork of GanjoorService that serves classical Urdu poetry and prose. See [DIWAN.md](DIWAN.md) for what changed and how to deploy it. The original README follows.
|
# دیوان · Diwan site
|
||||||
|
|
||||||
# GanjoorService
|
Diwan is a standalone project built on a fork of [GanjoorService](https://github.com/ganjoor/GanjoorService) (GPL-3.0), the software behind ganjoor.net, adapted to serve **classical Urdu poetry and prose** from [diwan-data](https://github.com/anas-rashid/diwan-data).
|
||||||
Ganjoor museum and ganjoor.net own backend (ASP.NET Core Web API) and frontend (Razor Pages) code
|
|
||||||
|
|
||||||
این کد وب سرویس [گنجینهٔ گنجور](https://museum.ganjoor.net) و [گنجور](https://ganjoor.net) و همچنین کد سایت گنجور است.
|
## Changes from upstream
|
||||||
|
|
||||||
[فهرست توابع در دسترس](https://api.ganjoor.net)
|
- **Tajik removed:** the TajikGanjoor site, Tajik API endpoints, services, export and transliteration are gone. Migration `DiwanRemoveTajik` drops their tables.
|
||||||
|
|
||||||

|
- **Branding:** "گنجور" becomes "دیوان" throughout the site. Upstream is credited in the footer.
|
||||||
|
- **Urdu basics:** pages are `lang="ur"`. The home page, footer and century groups (Hijri centuries, e.g. "تیرہویں صدی ہجری") are in Urdu. Deeper pages, such as admin and account pages, are still Persian.
|
||||||
|
- **Fonts:** Noto Nastaliq Urdu by default, with a **نستعلیق / نسخ** switch (Noto Naskh Arabic) at the bottom left. The choice is remembered per browser.
|
||||||
|
- **Footer:** links to Ganjoor-only services (Hafez divination, music index, etc.) are removed. Links to the Wikisource source, the data and the code are added.
|
||||||
|
- **Linux/Docker:** `Dockerfile` + `docker-compose.yml` (SQL Server 2022, API, site, Caddy for HTTPS).
|
||||||
|
- **Config fixes so env vars work:** the four places that read `appsettings.json` directly now also read environment variables. The JWT issuer follows `RSecurityBackend:ApplicationName` instead of the hard-coded "Ganjoor". `deploy/entrypoint.sh` copies the settings that RSecurityBackend reads only from `appsettings.json` (connection string, secret, app name, admin email) into the file at container start.
|
||||||
|
- **Links:** `ganjoor.net` links to the site's own pages are now relative. Links to Ganjoor's other services (blog, audio, etc.) are left as they are.
|
||||||
|
- **Locale:** `ur-PK`.
|
||||||
|
|
||||||
## Running it locally
|
## Deploy (Ubuntu/Debian x86-64, e.g. Vultr)
|
||||||
|
|
||||||
New to this codebase and want to run your own copy? See **[RUNNING_LOCALLY.md](RUNNING_LOCALLY.md)**
|
```sh
|
||||||
for a full step-by-step guide — cloning, database setup, and Visual Studio configuration.
|
# 1. Docker
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
|
||||||
The one thing that guide covers in more depth but is worth knowing up front: **the production
|
# 2. Code + config
|
||||||
database is never published**, since it contains private/user-linked data. What *is* published is
|
git clone https://github.com/anas-rashid/diwan.git && cd diwan
|
||||||
a git repository of the poetry content itself (poets, categories, poems — allowlisted, no user
|
cp .env.example .env && nano .env # domains, passwords, admin email
|
||||||
data): **[github.com/ganjoor/ganjoor-data](https://github.com/ganjoor/ganjoor-data)**. A fresh
|
|
||||||
local install can pull real content from there via **Admin → مالی و سایت → درونریزی دادهٔ عمومی**
|
|
||||||
(also reachable automatically the first time you run the site against an empty database) instead
|
|
||||||
of starting from nothing.
|
|
||||||
|
|
||||||
## Semantic search
|
# 3. DNS: point SITE_DOMAIN and API_DOMAIN (A records) at the server, then:
|
||||||
|
docker compose up -d --build # first build takes a few minutes
|
||||||
|
docker compose logs -f api # wait for "Application started"
|
||||||
|
```
|
||||||
|
|
||||||
The "find a poem about..." feature has its own setup guide, separate from the main one above —
|
SQL Server needs about 2 GB of RAM. Use a plan with at least 4 GB in total.
|
||||||
it needs a downloaded ONNX model and published embeddings data on top of the usual database setup:
|
|
||||||
**[SEMANTIC_SEARCH_SETUP.md](SEMANTIC_SEARCH_SETUP.md)**. Also has a real troubleshooting section
|
|
||||||
covering the actual problems hit building this feature, not a generic checklist — worth reading
|
|
||||||
before assuming something new is broken.
|
|
||||||
|
|
||||||
## Changelog
|
### Load the data
|
||||||
|
|
||||||
The user-facing changelog shown in the site's footer ("تازههای نرمافزار") is generated and
|
1. Open `https://SITE_DOMAIN/login` and sign in with `ADMIN_EMAIL` and the password **`Test!123`**. The first login creates the admin account with that fixed password (RSecurityBackend's default; upstream's guide is wrong about this). **Change it right away** in the user panel.
|
||||||
maintained from git history. See **[docs/CHANGELOG_PROCESS.md](docs/CHANGELOG_PROCESS.md)** for
|
2. On the import page that opens (or **Admin → مالی و سایت → درونریزی دادهٔ عمومی**), choose **Internet URL** and enter:
|
||||||
the JSON format and the process for adding new entries.
|
```
|
||||||
|
https://cdn.jsdelivr.net/gh/anas-rashid/diwan-data@main/
|
||||||
|
```
|
||||||
|
3. The import runs in the background (about 11k poems). Century groups are rebuilt automatically when it finishes.
|
||||||
|
|
||||||
|
Re-running the import adds new poems and leaves existing ones untouched, so it can be repeated after diwan-data's daily sync.
|
||||||
|
|
||||||
|
### Update
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git pull && docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build locally (macOS/Linux)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd RMuseum # its global.json pins SDK 10.0.302; newer SDKs fail on some upstream Razor views
|
||||||
|
dotnet build RMuseum.csproj -p:EnableWindowsTargeting=true
|
||||||
|
dotnet build ../GanjooRazor/GanjooRazor.csproj -p:EnableWindowsTargeting=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Running it needs SQL Server, so use the Docker setup above. SQL Server's image is x86-64 only.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GPL-3.0, same as upstream (see `LICENSE`). Data: see [diwan-data](https://github.com/anas-rashid/diwan-data).
|
||||||
|
|||||||
@ -2500,32 +2500,6 @@ namespace RMuseum.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// start exporting the Tajik (Cyrillic) overlay data to its own, separate public
|
|
||||||
/// git-tracked JSON data set — only poets/categories/poems with an actual Tajik
|
|
||||||
/// translation are included
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpPost("tajik/publicdata/batchexport")]
|
|
||||||
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
|
|
||||||
public IActionResult StartBatchExportTajikPublicGitData()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var res = _ganjoorService.StartBatchExportTajikPublicGitData();
|
|
||||||
if (!string.IsNullOrEmpty(res.ExceptionString))
|
|
||||||
return BadRequest(res.ExceptionString);
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
return BadRequest(exp.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// (re)build local Ganjoor content from a public data export tree (local folder or HTTP) —
|
/// (re)build local Ganjoor content from a public data export tree (local folder or HTTP) —
|
||||||
/// intended for local development use, not for production servers
|
/// intended for local development use, not for production servers
|
||||||
@ -2904,25 +2878,6 @@ namespace RMuseum.Controllers
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// build tj.ganjoor.net site map
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpPost("sitemap/tajik")]
|
|
||||||
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + SecurableItem.ModifyOperationShortName)]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
|
||||||
public IActionResult StartBuildingTajikSitemap()
|
|
||||||
{
|
|
||||||
var res = _ganjoorService.StartBuildingTajikSitemap();
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(res.ExceptionString))
|
|
||||||
return BadRequest(res.ExceptionString);
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// regenerate stats page
|
/// regenerate stats page
|
||||||
@ -4637,161 +4592,6 @@ namespace RMuseum.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
/// <summary>
|
|
||||||
/// import tajik from sqlite
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="poetId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpPost]
|
|
||||||
[Route("tajik/sqlite/import/{poetId}")]
|
|
||||||
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
|
||||||
public async Task<IActionResult> TajikImportFromSqlite(int poetId = 0)
|
|
||||||
{
|
|
||||||
IFormFile file = Request.Form.Files[0];
|
|
||||||
|
|
||||||
RServiceResult<bool> res =
|
|
||||||
await _ganjoorService.TajikImportFromSqlite(poetId, file);
|
|
||||||
if (res.Result)
|
|
||||||
return Ok();
|
|
||||||
return BadRequest(res.ExceptionString);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
[HttpPut]
|
|
||||||
[Route("tajik/sqlite/onetime/import")]
|
|
||||||
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
|
||||||
public IActionResult TajikOneTimeImportFromSqlite()
|
|
||||||
{
|
|
||||||
|
|
||||||
RServiceResult<bool> res =
|
|
||||||
_ganjoorService.TajikImportFromSqlite(0, "C:\\Tools\\import\\output.s3db");
|
|
||||||
if (res.Result)
|
|
||||||
return Ok();
|
|
||||||
return BadRequest(res.ExceptionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// re-runs the Tajik poet/category HTML generators for every already-imported poet and
|
|
||||||
/// category page, overwriting their stored TajikHtmlText. Needed one-time after changing
|
|
||||||
/// either generator function (e.g. adding poem excerpts to the table of contents), since
|
|
||||||
/// the normal SQLite import skips pages that already exist and never refreshes them.
|
|
||||||
/// Runs as a background job - check the Admin area's LongRunningJobs page for progress.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpPut]
|
|
||||||
[Route("tajik/regenerate-html")]
|
|
||||||
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
|
||||||
public IActionResult RegenerateTajikCatAndPoetHtmlTextAsync()
|
|
||||||
{
|
|
||||||
RServiceResult<bool> res =
|
|
||||||
_ganjoorService.RegenerateTajikCatAndPoetHtmlTextAsync();
|
|
||||||
if (res.Result)
|
|
||||||
return Ok();
|
|
||||||
return BadRequest(res.ExceptionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// recovery job: regenerates TajikHtmlText for Tajik poems whose stored HTML currently
|
|
||||||
/// contains a link (the signature left by the CatPage id corruption bug) - use after
|
|
||||||
/// RegenerateTajikCatAndPoetHtmlText (or anything else) may have corrupted poem pages.
|
|
||||||
/// Check the Admin area's LongRunningJobs page for progress.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpPut]
|
|
||||||
[Route("tajik/restore-poem-html")]
|
|
||||||
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(bool))]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
|
||||||
public IActionResult RestoreTajikPoemHtmlTextAsync()
|
|
||||||
{
|
|
||||||
RServiceResult<bool> res =
|
|
||||||
_ganjoorService.RestoreTajikPoemHtmlTextAsync();
|
|
||||||
if (res.Result)
|
|
||||||
return Ok();
|
|
||||||
return BadRequest(res.ExceptionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik poets
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpGet]
|
|
||||||
[Route("tajik/poets")]
|
|
||||||
[AllowAnonymous]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorTajikPoet[]))]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
|
||||||
public async Task<IActionResult> GetTajikPoetsAsync()
|
|
||||||
{
|
|
||||||
var cacheKey = $"ganjoor/tajik/poets";
|
|
||||||
if (!_memoryCache.TryGetValue(cacheKey, out GanjoorTajikPoet[] poets))
|
|
||||||
{
|
|
||||||
RServiceResult<GanjoorTajikPoet[]> res =
|
|
||||||
await _ganjoorService.GetTajikPoetsAsync();
|
|
||||||
if (!string.IsNullOrEmpty(res.ExceptionString))
|
|
||||||
return BadRequest(res.ExceptionString);
|
|
||||||
|
|
||||||
poets = res.Result;
|
|
||||||
if (AggressiveCacheEnabled)
|
|
||||||
_memoryCache.Set(cacheKey, poets, TimeSpan.FromHours(1));
|
|
||||||
|
|
||||||
}
|
|
||||||
return Ok(poets);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik page by url
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="url"></param>
|
|
||||||
/// <param name="catPoems"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpGet]
|
|
||||||
[Route("tajik/page")]
|
|
||||||
[AllowAnonymous]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(GanjoorPageCompleteViewModel))]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
|
||||||
public async Task<IActionResult> GetTajikPageByUrlAsync(string url, bool catPoems = false)
|
|
||||||
{
|
|
||||||
RServiceResult<GanjoorPageCompleteViewModel> res =
|
|
||||||
await _ganjoorService.GetTajikPageByUrlAsync(url, catPoems);
|
|
||||||
if (!string.IsNullOrEmpty(res.ExceptionString))
|
|
||||||
return BadRequest(res.ExceptionString);
|
|
||||||
if (res.Result == null)
|
|
||||||
return NotFound();
|
|
||||||
return Ok(res.Result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// search tajik poems
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="paging"></param>
|
|
||||||
/// <param name="term"></param>
|
|
||||||
/// <param name="poetId"></param>
|
|
||||||
/// <param name="catId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[HttpGet]
|
|
||||||
[Route("tajik/search")]
|
|
||||||
[AllowAnonymous]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(IEnumerable<GanjoorPoemCompleteViewModel>))]
|
|
||||||
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
|
|
||||||
|
|
||||||
public async Task<IActionResult> SearchTajikAsync([FromQuery] PagingParameterModel paging, string term, int poetId = 0, int catId = 0)
|
|
||||||
{
|
|
||||||
var pagedResult = await _ganjoorService.SearchTajikAsync(paging, term, poetId == 0 ? (int?)null : poetId, catId == 0 ? (int?)null : catId);
|
|
||||||
if (!string.IsNullOrEmpty(pagedResult.ExceptionString))
|
|
||||||
return BadRequest(pagedResult.ExceptionString);
|
|
||||||
|
|
||||||
// Paging Header
|
|
||||||
HttpContext.Response.Headers.Append("paging-headers", JsonConvert.SerializeObject(pagedResult.Result.PagingMeta));
|
|
||||||
|
|
||||||
return Ok(pagedResult.Result.Items);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// fill couplet summaries using open ai
|
/// fill couplet summaries using open ai
|
||||||
|
|||||||
@ -743,31 +743,6 @@ namespace RMuseum.DbContext
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<CategoryWordCountSummary> CategoryWordCountSummaries { get; set; }
|
public DbSet<CategoryWordCountSummary> CategoryWordCountSummaries { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik poets
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<GanjoorTajikPoet> TajikPoets { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik cats
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<GanjoorTajikCat> TajikCats { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik poems
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<GanjoorTajikPoem> TajikPoems { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik verse
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<GanjoorTajikVerse> TajikVerses { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik pages
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<GanjoorTajikPage> TajikPages { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ganjoor cat corrections
|
/// ganjoor cat corrections
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
6507
RMuseum/Migrations/20261004213712_DiwanRemoveTajik.Designer.cs
generated
Normal file
125
RMuseum/Migrations/20261004213712_DiwanRemoveTajik.cs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace RMuseum.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DiwanRemoveTajik : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TajikPages");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TajikPoems");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TajikVerses");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TajikCats");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "TajikPoets");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TajikPages",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false),
|
||||||
|
TajikHtmlText = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TajikPages", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TajikPoets",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false),
|
||||||
|
BirthYearInLHijri = table.Column<int>(type: "int", nullable: false),
|
||||||
|
TajikDescription = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
TajikNickname = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TajikPoets", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TajikVerses",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false),
|
||||||
|
PoemId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
TajikText = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
VOrder = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TajikVerses", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TajikCats",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false),
|
||||||
|
PoetId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
TajikDescription = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
TajikTitle = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TajikCats", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TajikCats_TajikPoets_PoetId",
|
||||||
|
column: x => x.PoetId,
|
||||||
|
principalTable: "TajikPoets",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "TajikPoems",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false),
|
||||||
|
CatId = table.Column<int>(type: "int", nullable: false),
|
||||||
|
FullTitle = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
FullUrl = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
TajikPlainText = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
TajikTitle = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_TajikPoems", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_TajikPoems_TajikCats_CatId",
|
||||||
|
column: x => x.CatId,
|
||||||
|
principalTable: "TajikCats",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TajikCats_PoetId",
|
||||||
|
table: "TajikCats",
|
||||||
|
column: "PoetId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_TajikPoems_CatId",
|
||||||
|
table: "TajikPoems",
|
||||||
|
column: "CatId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2497,105 +2497,6 @@ namespace RMuseum.Migrations
|
|||||||
b.ToTable("GanjoorSiteBanners");
|
b.ToTable("GanjoorSiteBanners");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorTajikCat", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("PoetId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("TajikDescription")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("TajikTitle")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("PoetId");
|
|
||||||
|
|
||||||
b.ToTable("TajikCats");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorTajikPage", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("TajikHtmlText")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("TajikPages");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorTajikPoem", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("CatId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("FullTitle")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("FullUrl")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("TajikPlainText")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("TajikTitle")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CatId");
|
|
||||||
|
|
||||||
b.ToTable("TajikPoems");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorTajikPoet", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("BirthYearInLHijri")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("TajikDescription")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<string>("TajikNickname")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("TajikPoets");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorTajikVerse", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("PoemId")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<string>("TajikText")
|
|
||||||
.HasColumnType("nvarchar(max)");
|
|
||||||
|
|
||||||
b.Property<int>("VOrder")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("TajikVerses");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorUserBookmark", b =>
|
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorUserBookmark", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -5815,28 +5716,6 @@ namespace RMuseum.Migrations
|
|||||||
b.Navigation("RImage");
|
b.Navigation("RImage");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorTajikCat", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("RMuseum.Models.Ganjoor.GanjoorTajikPoet", "Poet")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PoetId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Poet");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorTajikPoem", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("RMuseum.Models.Ganjoor.GanjoorTajikCat", "Cat")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("CatId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Cat");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorUserBookmark", b =>
|
modelBuilder.Entity("RMuseum.Models.Ganjoor.GanjoorUserBookmark", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("RMuseum.Models.Ganjoor.GanjoorPoem", "Poem")
|
b.HasOne("RMuseum.Models.Ganjoor.GanjoorPoem", "Poem")
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace RMuseum.Models.Ganjoor
|
|
||||||
{
|
|
||||||
public class GanjoorTajikCat
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// id
|
|
||||||
/// </summary>
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik title
|
|
||||||
/// </summary>
|
|
||||||
public string TajikTitle { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// additional description or note in Tajik
|
|
||||||
/// </summary>
|
|
||||||
public string TajikDescription { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// poet_id
|
|
||||||
/// </summary>
|
|
||||||
public int PoetId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// poet
|
|
||||||
/// </summary>
|
|
||||||
public GanjoorTajikPoet Poet { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace RMuseum.Models.Ganjoor
|
|
||||||
{
|
|
||||||
public class GanjoorTajikPage
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// id
|
|
||||||
/// </summary>
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// verses text as html (ganjoor.net format)
|
|
||||||
/// </summary>
|
|
||||||
public string TajikHtmlText { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace RMuseum.Models.Ganjoor
|
|
||||||
{
|
|
||||||
public class GanjoorTajikPoem
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// id
|
|
||||||
/// </summary>
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// cat id
|
|
||||||
/// </summary>
|
|
||||||
public int CatId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// title
|
|
||||||
/// </summary>
|
|
||||||
public string TajikTitle { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// verses text
|
|
||||||
/// </summary>
|
|
||||||
public string TajikPlainText { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// sample: /hafez/ghazal/sh1
|
|
||||||
/// </summary>
|
|
||||||
public string FullUrl { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// cat + parent cats title + title
|
|
||||||
/// </summary>
|
|
||||||
public string FullTitle { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// cat
|
|
||||||
/// </summary>
|
|
||||||
public GanjoorTajikCat Cat { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace RMuseum.Models.Ganjoor
|
|
||||||
{
|
|
||||||
public class GanjoorTajikPoet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// id
|
|
||||||
/// </summary>
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik nick name
|
|
||||||
/// </summary>
|
|
||||||
public string TajikNickname { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// additional description or note in Tajik
|
|
||||||
/// </summary>
|
|
||||||
public string TajikDescription { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// birth year in lunar hijri
|
|
||||||
/// </summary>
|
|
||||||
public int BirthYearInLHijri { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace RMuseum.Models.Ganjoor
|
|
||||||
{
|
|
||||||
public class GanjoorTajikVerse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// id
|
|
||||||
/// </summary>
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// poem_id
|
|
||||||
/// </summary>
|
|
||||||
public int PoemId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// vorder
|
|
||||||
/// </summary>
|
|
||||||
public int VOrder { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik text
|
|
||||||
/// </summary>
|
|
||||||
public string TajikText { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace RMuseum.Models.Ganjoor.PublicExport
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Root manifest for the Tajik data export — a separate, sparser repository from the main
|
|
||||||
/// ganjoor-data one. Tajik translation only covers a subset of poets/categories/poems, so this
|
|
||||||
/// export only ever contains entries that actually have a Tajik counterpart in the database
|
|
||||||
/// (GanjoorTajikPoet/Cat/Poem/Verse) — it does not mirror the full corpus the way the main
|
|
||||||
/// export does. Ids and FullUrls match the main repo 1:1 (same underlying entity ids), so a
|
|
||||||
/// consumer resolves structure/metre/sections for a poem by fetching the same id/path from
|
|
||||||
/// ganjoor-data and merging verses by VOrder.
|
|
||||||
/// </summary>
|
|
||||||
public class TajikPublicExportManifestDto
|
|
||||||
{
|
|
||||||
public int SchemaVersion { get; set; } = 1;
|
|
||||||
public string GeneratedAtUtc { get; set; }
|
|
||||||
public int PoetsCount { get; set; }
|
|
||||||
public int PoemsCount { get; set; }
|
|
||||||
public int IdIndexShardSize { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// where to find the structural data (metre, rhyme, sections, full verse text in Persian)
|
|
||||||
/// this repo's ids/paths correspond to
|
|
||||||
/// </summary>
|
|
||||||
public string MainDataRepo { get; set; } = "https://github.com/ganjoor/ganjoor-data";
|
|
||||||
|
|
||||||
public TajikPublicExportUrlTemplatesDto UrlTemplates { get; set; } = new TajikPublicExportUrlTemplatesDto();
|
|
||||||
|
|
||||||
public List<PublicExportManifestPoetEntryDto> Poets { get; set; } = new List<PublicExportManifestPoetEntryDto>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TajikPublicExportUrlTemplatesDto
|
|
||||||
{
|
|
||||||
public string Poet { get; set; } = "poets/{poetSlug}/poet.json";
|
|
||||||
public string Category { get; set; } = "poets/{poetSlug}/{catPath}/_cat.json";
|
|
||||||
public string Poem { get; set; } = "poets/{poetSlug}/{catPath}/{poemSlug}.json";
|
|
||||||
public string PoetIdIndex { get; set; } = "index/poets-by-id.json";
|
|
||||||
public string CatIdIndexShard { get; set; } = "index/cats-by-id/{bucket}.json";
|
|
||||||
public string PoemIdIndexShard { get; set; } = "index/poems-by-id/{bucket}.json";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// poet.json — only written for a poet with an actual GanjoorTajikPoet record.
|
|
||||||
/// FullUrl matches the poet's path in the main ganjoor-data repo exactly.
|
|
||||||
/// </summary>
|
|
||||||
public class TajikPoetPublicDto
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string TajikNickname { get; set; }
|
|
||||||
public string TajikDescription { get; set; }
|
|
||||||
public int BirthYearInLHijri { get; set; }
|
|
||||||
public string FullUrl { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// _cat.json — only written for a category with an actual GanjoorTajikCat record.
|
|
||||||
/// ChildCats/Poems only list children that themselves have a Tajik translation — a consumer
|
|
||||||
/// never gets a link to a file that doesn't exist.
|
|
||||||
/// </summary>
|
|
||||||
public class TajikCatPublicDto
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int PoetId { get; set; }
|
|
||||||
public int? ParentId { get; set; }
|
|
||||||
public string TajikTitle { get; set; }
|
|
||||||
public string TajikDescription { get; set; }
|
|
||||||
public string FullUrl { get; set; }
|
|
||||||
public List<CatChildRefDto> ChildCats { get; set; } = new List<CatChildRefDto>();
|
|
||||||
public List<PoemChildRefDto> Poems { get; set; } = new List<PoemChildRefDto>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// {poem-slug}.json — only written for a poem with an actual GanjoorTajikPoem record.
|
|
||||||
/// Deliberately doesn't carry metre/rhyme/section data — GanjoorTajikPoem/Verse don't store
|
|
||||||
/// their own copy of that either; fetch it from the same id/path in the main ganjoor-data repo.
|
|
||||||
/// </summary>
|
|
||||||
public class TajikPoemPublicDto
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public int CatId { get; set; }
|
|
||||||
public string TajikTitle { get; set; }
|
|
||||||
public string FullTitle { get; set; }
|
|
||||||
public string FullUrl { get; set; }
|
|
||||||
public string TajikPlainText { get; set; }
|
|
||||||
public List<TajikVersePublicDto> Verses { get; set; } = new List<TajikVersePublicDto>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// VOrder is the join key back to the corresponding verse in the main repo's poem file — no
|
|
||||||
/// position/section/couplet data is duplicated here, matching how GanjoorTajikVerse itself
|
|
||||||
/// stores nothing but the transliterated text and its order.
|
|
||||||
/// </summary>
|
|
||||||
public class TajikVersePublicDto
|
|
||||||
{
|
|
||||||
public int VOrder { get; set; }
|
|
||||||
public string TajikText { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -60,8 +60,7 @@
|
|||||||
public string CoupletSummary { get; set; }
|
public string CoupletSummary { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// original (Persian) text - only populated for Tajik responses that need to show
|
/// original text (e.g. before a correction). This is a pure view
|
||||||
/// the Persian original alongside the Tajik transliteration. This is a pure view
|
|
||||||
/// model (not an EF entity), so no [NotMapped] or migration concerns here.
|
/// model (not an EF entity), so no [NotMapped] or migration concerns here.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string OriginalText { get; set; }
|
public string OriginalText { get; set; }
|
||||||
|
|||||||
@ -650,13 +650,6 @@ namespace RMuseum.Services
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
RServiceResult<bool> StartBatchExportPublicGitData();
|
RServiceResult<bool> StartBatchExportPublicGitData();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// start exporting the Tajik (Cyrillic) overlay data to its own separate git-tracked JSON
|
|
||||||
/// data set — only entities that actually have a Tajik translation are included
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
RServiceResult<bool> StartBatchExportTajikPublicGitData();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// (re)build local Ganjoor content (poets/categories/poems/verses/sections + their pages)
|
/// (re)build local Ganjoor content (poets/categories/poems/verses/sections + their pages)
|
||||||
/// from a public data export tree, read locally or over HTTP. Safe to re-run — existing
|
/// from a public data export tree, read locally or over HTTP. Safe to re-run — existing
|
||||||
@ -863,12 +856,6 @@ namespace RMuseum.Services
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
RServiceResult<bool> StartBuildingSitemap();
|
RServiceResult<bool> StartBuildingSitemap();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// build tj.ganjoor.net site map
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
RServiceResult<bool> StartBuildingTajikSitemap();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// start updating stats page
|
/// start updating stats page
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -1584,55 +1571,6 @@ namespace RMuseum.Services
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<RServiceResult<CategoryWordCount>> GetCategoryWordCountByTermAsync(string term, int? catId, int? poetId);
|
Task<RServiceResult<CategoryWordCount>> GetCategoryWordCountByTermAsync(string term, int? catId, int? poetId);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik import from sqlite
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="poetId"></param>
|
|
||||||
/// <param name="filePath"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
RServiceResult<bool> TajikImportFromSqlite(int poetId, string filePath);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik poets
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task<RServiceResult<GanjoorTajikPoet[]>> GetTajikPoetsAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik page by url
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="url"></param>
|
|
||||||
/// <param name="catPoems"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task<RServiceResult<GanjoorPageCompleteViewModel>> GetTajikPageByUrlAsync(string url, bool catPoems = false);
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// search tajik poems
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="paging"></param>
|
|
||||||
/// <param name="term"></param>
|
|
||||||
/// <param name="poetId"></param>
|
|
||||||
/// <param name="catId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task<RServiceResult<(PaginationMetadata PagingMeta, GanjoorPoemCompleteViewModel[] Items)>> SearchTajikAsync(PagingParameterModel paging, string term, int? poetId, int? catId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// re-runs the Tajik poet/category HTML generators for every already-imported poet and
|
|
||||||
/// category page, overwriting their stored TajikHtmlText - needed one-time after changing
|
|
||||||
/// either generator function, since the normal SQLite import skips pages that already exist.
|
|
||||||
/// Runs as a background job; returns immediately once the job is queued.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
RServiceResult<bool> RegenerateTajikCatAndPoetHtmlTextAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// recovery job: regenerates TajikHtmlText for Tajik poems whose stored HTML currently
|
|
||||||
/// contains a link (the signature left by the CatPage id corruption bug - correct poem
|
|
||||||
/// HTML never contains one), overwriting just those with correct verse markup.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
RServiceResult<bool> RestoreTajikPoemHtmlTextAsync();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -137,80 +137,5 @@ namespace RMuseum.Services.Implementation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// build tj.ganjoor.net site map
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public RServiceResult<bool> StartBuildingTajikSitemap()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
_backgroundTaskQueue.QueueBackgroundWorkItem
|
|
||||||
(
|
|
||||||
async token =>
|
|
||||||
{
|
|
||||||
using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions<RMuseumDbContext>())) //this is long running job, so _context might be already been freed/collected by GC
|
|
||||||
{
|
|
||||||
LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context);
|
|
||||||
var job = (await jobProgressServiceEF.NewJob("BuildTajikSitemap", "Query data")).Result;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string xmlSitemap = Configuration.GetSection("Ganjoor")["TajikSitemapLocation"];
|
|
||||||
if (File.Exists(xmlSitemap))
|
|
||||||
File.Delete(xmlSitemap);
|
|
||||||
|
|
||||||
string dir = Path.GetDirectoryName(xmlSitemap);
|
|
||||||
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 0, "", false);
|
|
||||||
|
|
||||||
List<string> sitemaps = new List<string>();
|
|
||||||
|
|
||||||
foreach (var poet in await context.TajikPoets.AsNoTracking().ToListAsync())
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, poet.Id, "", false);
|
|
||||||
|
|
||||||
string poetSitemap = Path.Combine(dir, $"{poet.Id}.xml");
|
|
||||||
|
|
||||||
WriteSitemap(poetSitemap,
|
|
||||||
await context.GanjoorPages.Where(p => p.PoetId == poet.Id && (p.GanjoorPageType == Models.Ganjoor.GanjoorPageType.PoetPage || p.GanjoorPageType == Models.Ganjoor.GanjoorPageType.CatPage || p.GanjoorPageType == Models.Ganjoor.GanjoorPageType.PoemPage)).OrderBy(p => p.Id).Select(p => p.FullUrl).ToListAsync(),
|
|
||||||
"https://tj.ganjoor.net"
|
|
||||||
);
|
|
||||||
|
|
||||||
sitemaps.Add(poetSitemap);
|
|
||||||
}
|
|
||||||
|
|
||||||
XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9";
|
|
||||||
XNamespace xsiNs = "http://www.w3.org/2001/XMLSchema-instance";
|
|
||||||
XDocument xDoc = new XDocument(
|
|
||||||
new XDeclaration("1.0", "UTF-8", "no"),
|
|
||||||
new XElement(ns + "sitemapindex",
|
|
||||||
new XAttribute(XNamespace.Xmlns + "xsi", xsiNs),
|
|
||||||
new XAttribute(xsiNs + "schemaLocation",
|
|
||||||
"http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"),
|
|
||||||
from sitemap in sitemaps
|
|
||||||
select new XElement(ns + "sitemap",
|
|
||||||
new XElement(ns + "loc", $"https://tj.ganjoor.net/{Path.GetFileName(sitemap)}"))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
xDoc.Save(xmlSitemap);
|
|
||||||
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true);
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return new RServiceResult<bool>(true);
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
return new RServiceResult<bool>(false, exp.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,640 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System;
|
|
||||||
using RMuseum.Models.Ganjoor;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using RSecurityBackend.Models.Generic;
|
|
||||||
using RMuseum.Models.Ganjoor.ViewModels;
|
|
||||||
using System.Linq;
|
|
||||||
using RSecurityBackend.Services.Implementation;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using RMuseum.DbContext;
|
|
||||||
|
|
||||||
namespace RMuseum.Services.Implementation
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// IGanjoorService implementation
|
|
||||||
/// </summary>
|
|
||||||
public partial class GanjoorService : IGanjoorService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// tajik poets
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<RServiceResult<GanjoorTajikPoet[]>> GetTajikPoetsAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new RServiceResult<GanjoorTajikPoet[]>(await _context.TajikPoets.AsNoTracking().ToArrayAsync());
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
return new RServiceResult<GanjoorTajikPoet[]>(null, exp.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// tajik page by url
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="url"></param>
|
|
||||||
/// <param name="catPoems"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<RServiceResult<GanjoorPageCompleteViewModel>> GetTajikPageByUrlAsync(string url, bool catPoems = false)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var res = await GetPageByUrl(url, catPoems);
|
|
||||||
if (!string.IsNullOrEmpty(res.ExceptionString))
|
|
||||||
return res;
|
|
||||||
GanjoorPageCompleteViewModel page = res.Result;
|
|
||||||
|
|
||||||
int poetId = page.Poem != null ? page.Poem.Category.Poet.Id : page.PoetOrCat.Poet.Id;
|
|
||||||
|
|
||||||
var tajikPage = await _context.TajikPages.AsNoTracking().Where(p => p.Id == page.Id).SingleOrDefaultAsync();
|
|
||||||
if (tajikPage == null)
|
|
||||||
return new RServiceResult<GanjoorPageCompleteViewModel>(null, "Ин сафҳа ҳануз ба тоҷикии баргардонида нашуда.");
|
|
||||||
|
|
||||||
page.HtmlText = tajikPage.TajikHtmlText;
|
|
||||||
|
|
||||||
var tajikPoet = await _context.TajikPoets.AsNoTracking().Where(p => p.Id == poetId).SingleOrDefaultAsync();
|
|
||||||
if (tajikPoet == null)
|
|
||||||
return new RServiceResult<GanjoorPageCompleteViewModel>(null, "Ин суханвар ҳануз ба тоҷикӣ дар дастрас нест.");
|
|
||||||
if(page.PoetOrCat != null && page.PoetOrCat.Poet != null)
|
|
||||||
{
|
|
||||||
page.PoetOrCat.Poet.Nickname = tajikPoet.TajikNickname;
|
|
||||||
page.PoetOrCat.Poet.Description = tajikPoet.TajikDescription;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(page.Poem != null && page.Poem.Category != null && page.Poem.Category.Poet != null)
|
|
||||||
{
|
|
||||||
page.Poem.Category.Poet.Nickname = tajikPoet.TajikNickname;
|
|
||||||
page.Poem.Category.Poet.Description = tajikPoet.TajikDescription;
|
|
||||||
}
|
|
||||||
|
|
||||||
int catId = page.Poem != null ? page.Poem.Category.Cat.Id : page.PoetOrCat.Cat.Id;
|
|
||||||
|
|
||||||
var tajikCat = await _context.TajikCats.AsNoTracking().Where(c => c.Id == catId).SingleOrDefaultAsync();
|
|
||||||
if (tajikCat == null)
|
|
||||||
return new RServiceResult<GanjoorPageCompleteViewModel>(null, "Ин бахш ҳануз ба тоҷикӣ баргардонида нашудааст.");
|
|
||||||
if (page.Poem != null && page.Poem.Category != null && page.Poem.Category.Cat != null)
|
|
||||||
{
|
|
||||||
page.Poem.Category.Cat.Title = tajikCat.TajikTitle;
|
|
||||||
page.Poem.Category.Cat.Description = tajikCat.TajikDescription;
|
|
||||||
foreach (var parent in page.Poem.Category.Cat.Ancestors)
|
|
||||||
{
|
|
||||||
// ancestors/children/siblings are supplementary navigation, not the page's
|
|
||||||
// own content - a missing translation here shouldn't take down the whole
|
|
||||||
// page, so fall back to transliteration the same way the poem next/prev
|
|
||||||
// links already do (see LanguageUtils.TransliteratePersianToTajik)
|
|
||||||
var tajikParent = await _context.TajikCats.AsNoTracking().Where(c => c.Id == parent.Id).SingleOrDefaultAsync();
|
|
||||||
if (tajikParent != null)
|
|
||||||
{
|
|
||||||
parent.Title = tajikParent.TajikTitle;
|
|
||||||
parent.Description = tajikParent.TajikDescription;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
parent.Title = LanguageUtils.TransliteratePersianToTajik(parent.Title);
|
|
||||||
parent.Description = LanguageUtils.TransliteratePersianToTajik(parent.Description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (var child in page.Poem.Category.Cat.Children)
|
|
||||||
{
|
|
||||||
var tajikChild = await _context.TajikCats.AsNoTracking().Where(c => c.Id == child.Id).SingleOrDefaultAsync();
|
|
||||||
if (tajikChild != null)
|
|
||||||
{
|
|
||||||
child.Title = tajikChild.TajikTitle;
|
|
||||||
child.Description = tajikChild.TajikDescription;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
child.Title = LanguageUtils.TransliteratePersianToTajik(child.Title);
|
|
||||||
child.Description = LanguageUtils.TransliteratePersianToTajik(child.Description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (page.PoetOrCat != null && page.PoetOrCat.Cat != null)
|
|
||||||
{
|
|
||||||
page.PoetOrCat.Cat.Title = tajikCat.TajikTitle;
|
|
||||||
page.PoetOrCat.Cat.Description = tajikCat.TajikDescription;
|
|
||||||
foreach (var parent in page.PoetOrCat.Cat.Ancestors)
|
|
||||||
{
|
|
||||||
var tajikParent = await _context.TajikCats.AsNoTracking().Where(c => c.Id == parent.Id).SingleOrDefaultAsync();
|
|
||||||
if (tajikParent != null)
|
|
||||||
{
|
|
||||||
parent.Title = tajikParent.TajikTitle;
|
|
||||||
parent.Description = tajikParent.TajikDescription;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
parent.Title = LanguageUtils.TransliteratePersianToTajik(parent.Title);
|
|
||||||
parent.Description = LanguageUtils.TransliteratePersianToTajik(parent.Description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (var child in page.PoetOrCat.Cat.Children)
|
|
||||||
{
|
|
||||||
var tajikChild = await _context.TajikCats.AsNoTracking().Where(c => c.Id == child.Id).SingleOrDefaultAsync();
|
|
||||||
if (tajikChild != null)
|
|
||||||
{
|
|
||||||
child.Title = tajikChild.TajikTitle;
|
|
||||||
child.Description = tajikChild.TajikDescription;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
child.Title = LanguageUtils.TransliteratePersianToTajik(child.Title);
|
|
||||||
child.Description = LanguageUtils.TransliteratePersianToTajik(child.Description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (page.PoetOrCat.Cat.Next != null)
|
|
||||||
{
|
|
||||||
var nextCat = await _context.TajikCats.AsNoTracking().Where(c => c.Id == page.PoetOrCat.Cat.Next.Id).SingleOrDefaultAsync();
|
|
||||||
page.PoetOrCat.Cat.Next.Title = nextCat != null ? nextCat.TajikTitle : LanguageUtils.TransliteratePersianToTajik(page.PoetOrCat.Cat.Next.Title);
|
|
||||||
}
|
|
||||||
if (page.PoetOrCat.Cat.Previous != null)
|
|
||||||
{
|
|
||||||
var preCat = await _context.TajikCats.AsNoTracking().Where(c => c.Id == page.PoetOrCat.Cat.Previous.Id).SingleOrDefaultAsync();
|
|
||||||
page.PoetOrCat.Cat.Previous.Title = preCat != null ? preCat.TajikTitle : LanguageUtils.TransliteratePersianToTajik(page.PoetOrCat.Cat.Previous.Title);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (page.PoetOrCat.Cat.Poems != null)
|
|
||||||
{
|
|
||||||
var tajikPoems = await _context.TajikPoems.AsNoTracking().Where(p => p.CatId == catId).ToListAsync();
|
|
||||||
foreach (var poem in page.PoetOrCat.Cat.Poems)
|
|
||||||
{
|
|
||||||
var tajikPoem = tajikPoems.Where(p => p.Id == poem.Id).SingleOrDefault();
|
|
||||||
if (tajikPoem != null)
|
|
||||||
{
|
|
||||||
poem.Title = tajikPoem.TajikTitle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (page.Poem != null)
|
|
||||||
{
|
|
||||||
var tajikPoem = await _context.TajikPoems.AsNoTracking().Where(p => p.Id == page.Poem.Id).SingleAsync();
|
|
||||||
page.Poem.Title = tajikPoem.TajikTitle;
|
|
||||||
page.Poem.PlainText = tajikPoem.TajikPlainText;
|
|
||||||
|
|
||||||
|
|
||||||
var tajikVerses = await _context.TajikVerses.AsNoTracking().Where(v => v.PoemId == page.Poem.Id).OrderBy(v => v.VOrder).ToListAsync();
|
|
||||||
foreach( var verse in page.Poem.Verses)
|
|
||||||
{
|
|
||||||
verse.OriginalText = verse.Text;
|
|
||||||
verse.Text = tajikVerses.Where(v => v.VOrder == verse.VOrder).Single().TajikText;
|
|
||||||
}
|
|
||||||
// recompute fresh on every request (verses are already in memory) rather than
|
|
||||||
// relying on the precomputed TajikHtmlText blob - keeps this in sync with the
|
|
||||||
// verse data automatically, no regeneration job needed if it ever changes again
|
|
||||||
page.HtmlText = PrepareBilingualHtmlText(page.Poem.Verses);
|
|
||||||
|
|
||||||
if (page.Poem.Recitations != null)
|
|
||||||
{
|
|
||||||
// no TajikRecitations table exists - this audio metadata has no real Tajik
|
|
||||||
// translation source anywhere, so transliteration is the only option
|
|
||||||
foreach (var recitation in page.Poem.Recitations)
|
|
||||||
{
|
|
||||||
recitation.AudioTitle = LanguageUtils.TransliteratePersianToTajik(recitation.AudioTitle);
|
|
||||||
recitation.AudioArtist = LanguageUtils.TransliteratePersianToTajik(recitation.AudioArtist);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(page.Poem.Next != null)
|
|
||||||
{
|
|
||||||
var nextPoem = await _context.TajikPoems.AsNoTracking().Where(p => p.Id == page.Poem.Next.Id).SingleOrDefaultAsync();
|
|
||||||
if(nextPoem != null)
|
|
||||||
{
|
|
||||||
// real Tajik translation exists for the neighbouring poem - use it
|
|
||||||
page.Poem.Next.Title = nextPoem.TajikTitle;
|
|
||||||
page.Poem.Next.Excerpt = ExtractExcerpt(nextPoem.TajikPlainText);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// no Tajik translation for this poem yet - fall back to a simple
|
|
||||||
// best-effort letter transliteration rather than showing raw
|
|
||||||
// Perso-Arabic script (see LanguageUtils.TransliteratePersianToTajik)
|
|
||||||
page.Poem.Next.Title = LanguageUtils.TransliteratePersianToTajik(page.Poem.Next.Title);
|
|
||||||
page.Poem.Next.Excerpt = LanguageUtils.TransliteratePersianToTajik(page.Poem.Next.Excerpt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (page.Poem.Previous != null)
|
|
||||||
{
|
|
||||||
var prePoem = await _context.TajikPoems.AsNoTracking().Where(p => p.Id == page.Poem.Previous.Id).SingleOrDefaultAsync();
|
|
||||||
if (prePoem != null)
|
|
||||||
{
|
|
||||||
page.Poem.Previous.Title = prePoem.TajikTitle;
|
|
||||||
page.Poem.Previous.Excerpt = ExtractExcerpt(prePoem.TajikPlainText);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
page.Poem.Previous.Title = LanguageUtils.TransliteratePersianToTajik(page.Poem.Previous.Title);
|
|
||||||
page.Poem.Previous.Excerpt = LanguageUtils.TransliteratePersianToTajik(page.Poem.Previous.Excerpt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new RServiceResult<GanjoorPageCompleteViewModel>(page);
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
return new RServiceResult<GanjoorPageCompleteViewModel>(null, exp.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// short plain-text excerpt (first non-empty line) for a Tajik poem's next/previous nav link
|
|
||||||
/// </summary>
|
|
||||||
private static string ExtractExcerpt(string plainText)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(plainText)) return "";
|
|
||||||
var firstLine = plainText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? "";
|
|
||||||
return firstLine.Length > 80 ? firstLine.Substring(0, 80) + "…" : firstLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tajik counterpart of PrepareHtmlText (GanjoorService-SQLiteImport.cs) - same
|
|
||||||
/// VersePosition-based structure/CSS classes, but each verse's Persian original
|
|
||||||
/// (GanjoorVerse.OriginalText, set just before this is called) is rendered as an
|
|
||||||
/// extra <p class="tg-fa" dir="rtl"> line directly under its Tajik line, for
|
|
||||||
/// an interlinear/bilingual display. Computed fresh per-request rather than stored,
|
|
||||||
/// since it's cheap given the verses are already loaded for the response.
|
|
||||||
/// </summary>
|
|
||||||
private static string PrepareBilingualHtmlText(GanjoorVerseViewModel[] verses)
|
|
||||||
{
|
|
||||||
string FaLine(string text) => string.IsNullOrEmpty(text) ? "" : $"<p class=\"tg-fa\" dir=\"rtl\">{text}</p>";
|
|
||||||
|
|
||||||
string htmlText = "";
|
|
||||||
int coupletIndex = 0;
|
|
||||||
for (int vIndex = 0; vIndex < verses.Length; vIndex++)
|
|
||||||
{
|
|
||||||
GanjoorVerseViewModel v = verses[vIndex];
|
|
||||||
if (v.VersePosition == VersePosition.CenteredVerse1)
|
|
||||||
{
|
|
||||||
coupletIndex++;
|
|
||||||
if (((vIndex + 1) < verses.Length) && (verses[vIndex + 1].VersePosition == VersePosition.CenteredVerse2))
|
|
||||||
{
|
|
||||||
htmlText += $"<div class=\"b2\" id=\"bn{coupletIndex}\"><p>{v.Text}</p>{FaLine(v.OriginalText)}{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
htmlText += $"<div class=\"b2\" id=\"bn{coupletIndex}\"><p>{v.Text}</p>{FaLine(v.OriginalText)}</div>{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (v.VersePosition == VersePosition.CenteredVerse2)
|
|
||||||
{
|
|
||||||
htmlText += $"<p>{v.Text}</p>{FaLine(v.OriginalText)}</div>{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (v.VersePosition == VersePosition.Right)
|
|
||||||
{
|
|
||||||
coupletIndex++;
|
|
||||||
htmlText += $"<div class=\"b\" id=\"bn{coupletIndex}\"><div class=\"m1\"><p>{v.Text}</p>{FaLine(v.OriginalText)}</div>{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (v.VersePosition == VersePosition.Left)
|
|
||||||
{
|
|
||||||
htmlText += $"<div class=\"m2\"><p>{v.Text}</p>{FaLine(v.OriginalText)}</div></div>{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (v.VersePosition == VersePosition.Comment)
|
|
||||||
{
|
|
||||||
htmlText += $"<div class=\"c\"><p>{v.Text}</p>{FaLine(v.OriginalText)}</div>{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (v.VersePosition == VersePosition.Paragraph || v.VersePosition == VersePosition.Single)
|
|
||||||
{
|
|
||||||
coupletIndex++;
|
|
||||||
string[] lines = v.Text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
string[] faLines = string.IsNullOrEmpty(v.OriginalText) ? new string[0] : v.OriginalText.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
string cssClass = v.VersePosition == VersePosition.Paragraph ? "n" : "l";
|
|
||||||
|
|
||||||
if (lines.Length != 0)
|
|
||||||
{
|
|
||||||
if (v.Text.Length / lines.Length < 150)
|
|
||||||
{
|
|
||||||
htmlText += $"<div class=\"{cssClass}\" id=\"bn{coupletIndex}\"><p>{v.Text.Replace("\r\n", " ")}</p>{FaLine(v.OriginalText?.Replace("\r\n", " "))}</div>{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Persian original might not split into the exact same number of
|
|
||||||
// lines as its Tajik translation - matched by index on a best-effort
|
|
||||||
// basis; this branch only fires for long paragraph/note text, not the
|
|
||||||
// actual poem verses (Right/Left/CenteredVerse), so a rare misalignment
|
|
||||||
// here is a minor cosmetic issue, not a poem-text-accuracy one
|
|
||||||
for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++)
|
|
||||||
{
|
|
||||||
string faOne = lineIndex < faLines.Length ? faLines[lineIndex] : "";
|
|
||||||
htmlText += $"<div class=\"{cssClass}\" id=\"bn{coupletIndex}\"><p>{lines[lineIndex]}</p>{FaLine(faOne)}</div>{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(v.Text))
|
|
||||||
{
|
|
||||||
htmlText += $"<div class=\"{cssClass}\" id=\"bn{coupletIndex}\"><p> </p></div>{Environment.NewLine}";//empty line!
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
htmlText += $"<div class=\"{cssClass}\" id=\"bn{coupletIndex}\"><p>{v.Text}</p>{FaLine(v.OriginalText)}</div>{Environment.NewLine}";//not brave enough to ignore it!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return htmlText.Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// re-runs the Tajik poet/category HTML generators for every already-imported poet and
|
|
||||||
/// category page, overwriting their stored TajikHtmlText. Needed one-time after changing
|
|
||||||
/// either generator function (e.g. adding poem excerpts to the table of contents), since
|
|
||||||
/// the normal SQLite import skips pages that already exist and never refreshes them.
|
|
||||||
/// Runs as a background job (same pattern as TajikImportFromSqlite) since the number of
|
|
||||||
/// poets/categories/poems involved can be large enough to exceed a normal request timeout;
|
|
||||||
/// progress can be tracked the same way as other long running jobs (e.g. the Admin area's
|
|
||||||
/// LongRunningJobs page).
|
|
||||||
/// </summary>
|
|
||||||
public RServiceResult<bool> RegenerateTajikCatAndPoetHtmlTextAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_backgroundTaskQueue.QueueBackgroundWorkItem
|
|
||||||
(
|
|
||||||
async token =>
|
|
||||||
{
|
|
||||||
using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions<RMuseumDbContext>())) //this is long running job, so _context might be already been freed/collected by GC
|
|
||||||
{
|
|
||||||
LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context);
|
|
||||||
var job = (await jobProgressServiceEF.NewJob("RegenerateTajikCatAndPoetHtmlText", "Query data")).Result;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var tajikPoets = await context.TajikPoets.AsNoTracking().ToListAsync();
|
|
||||||
int done = 0;
|
|
||||||
foreach (var tajikPoet in tajikPoets)
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, done, $"poet: {tajikPoet.Id}");
|
|
||||||
var poetPage = await context.GanjoorPages.AsNoTracking().Where(p => p.GanjoorPageType == GanjoorPageType.PoetPage && p.PoetId == tajikPoet.Id).SingleOrDefaultAsync();
|
|
||||||
if (poetPage != null)
|
|
||||||
{
|
|
||||||
var tajikPage = await context.TajikPages.Where(p => p.Id == poetPage.Id).SingleOrDefaultAsync();
|
|
||||||
if (tajikPage != null)
|
|
||||||
{
|
|
||||||
tajikPage.TajikHtmlText = await PrepareTajikPoetHtmlTextAsync(context, tajikPoet);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
done++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tajikCats = await context.TajikCats.AsNoTracking().ToListAsync();
|
|
||||||
done = 0;
|
|
||||||
foreach (var tajikCat in tajikCats)
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, done, $"cat: {tajikCat.Id}");
|
|
||||||
// tajikCat.Id is the CATEGORY's own id (GanjoorCategories.Id), which is a
|
|
||||||
// completely different id space from GanjoorPages.Id (catPageId is generated
|
|
||||||
// independently at import time - see GanjoorService-SQLiteImport.cs). Must
|
|
||||||
// resolve the actual CatPage's GanjoorPages.Id first, exactly like the poet
|
|
||||||
// loop above already does - using tajikCat.Id directly here previously
|
|
||||||
// caused TajikPages rows to be looked up (and overwritten) by the wrong id,
|
|
||||||
// corrupting whichever unrelated page (often a poem) happened to have that id.
|
|
||||||
var catPage = await context.GanjoorPages.AsNoTracking().Where(p => p.GanjoorPageType == GanjoorPageType.CatPage && p.CatId == tajikCat.Id).SingleOrDefaultAsync();
|
|
||||||
if (catPage != null)
|
|
||||||
{
|
|
||||||
var tajikPage = await context.TajikPages.Where(p => p.Id == catPage.Id).SingleOrDefaultAsync();
|
|
||||||
if (tajikPage != null)
|
|
||||||
{
|
|
||||||
tajikPage.TajikHtmlText = await PrepareTajikCatHtmlTextAsync(context, tajikCat);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
done++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true);
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
return new RServiceResult<bool>(false, exp.ToString());
|
|
||||||
}
|
|
||||||
return new RServiceResult<bool>(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// recovery job: regenerates TajikHtmlText for every Tajik poem from its TajikVerses,
|
|
||||||
/// overwriting whatever is currently stored. GanjoorTajikVerse rows carry no VersePosition
|
|
||||||
/// of their own, so it's recovered by joining back to the original GanjoorVerses row with
|
|
||||||
/// the same Id (the SQLite import sets GanjoorTajikVerse.Id = the original verse's Id).
|
|
||||||
/// Only processes poems whose stored TajikHtmlText currently contains a link - correct poem
|
|
||||||
/// HTML (from PrepareHtmlText) is pure verse markup (.b/.m1/.m2/.n/.l) and never contains an
|
|
||||||
/// <a href=...>, so a link inside it is the signature the CatPage id bug left behind
|
|
||||||
/// (it overwrote the affected poems with category-style link-list HTML). Scoping to that
|
|
||||||
/// signature instead of every poem cuts the run down to just the actually-corrupted ones.
|
|
||||||
/// </summary>
|
|
||||||
public RServiceResult<bool> RestoreTajikPoemHtmlTextAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_backgroundTaskQueue.QueueBackgroundWorkItem
|
|
||||||
(
|
|
||||||
async token =>
|
|
||||||
{
|
|
||||||
using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions<RMuseumDbContext>()))
|
|
||||||
{
|
|
||||||
LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context);
|
|
||||||
var job = (await jobProgressServiceEF.NewJob("RestoreTajikPoemHtmlText", "Query data")).Result;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var corruptedPoemIds = await context.TajikPages
|
|
||||||
.Where(tp => tp.TajikHtmlText.Contains("<a href=") && context.TajikPoems.Any(pm => pm.Id == tp.Id))
|
|
||||||
.Select(tp => tp.Id)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
int done = 0;
|
|
||||||
foreach (var poemId in corruptedPoemIds)
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, done, $"poem: {poemId} ({done}/{corruptedPoemIds.Count})");
|
|
||||||
|
|
||||||
var tajikVerses = await context.TajikVerses.AsNoTracking().Where(v => v.PoemId == poemId).ToListAsync();
|
|
||||||
if (tajikVerses.Count > 0)
|
|
||||||
{
|
|
||||||
var originalVerses = await context.GanjoorVerses.AsNoTracking().Where(v => v.PoemId == poemId).ToListAsync();
|
|
||||||
var verses = tajikVerses
|
|
||||||
.Select(tv =>
|
|
||||||
{
|
|
||||||
var original = originalVerses.SingleOrDefault(v => v.Id == tv.Id);
|
|
||||||
return new GanjoorVerse()
|
|
||||||
{
|
|
||||||
VOrder = tv.VOrder,
|
|
||||||
VersePosition = original != null ? original.VersePosition : VersePosition.Paragraph,
|
|
||||||
Text = tv.TajikText,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.OrderBy(v => v.VOrder)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// TajikPages.Id == the poem's own Id for poem pages (see
|
|
||||||
// GanjoorService-TajikSQLiteImport.cs) - no GanjoorPages lookup needed here
|
|
||||||
var tajikPage = await context.TajikPages.Where(p => p.Id == poemId).SingleOrDefaultAsync();
|
|
||||||
if (tajikPage != null)
|
|
||||||
{
|
|
||||||
tajikPage.TajikHtmlText = PrepareHtmlText(verses);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
done++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true);
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
return new RServiceResult<bool>(false, exp.ToString());
|
|
||||||
}
|
|
||||||
return new RServiceResult<bool>(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Search
|
|
||||||
/// You need to run this scripts manually on the database before using this method:
|
|
||||||
///
|
|
||||||
/// CREATE FULLTEXT CATALOG [TajikPoemPlainTextCatalog] WITH ACCENT_SENSITIVITY = OFF AS DEFAULT
|
|
||||||
///
|
|
||||||
/// CREATE FULLTEXT INDEX ON [dbo].[TajikPoems](
|
|
||||||
/// [TajikPlainText] LANGUAGE 'English')
|
|
||||||
/// KEY INDEX [PK_TajikPoems] ON ([TajikPoemPlainTextCatalog], FILEGROUP [PRIMARY])
|
|
||||||
/// WITH (CHANGE_TRACKING = AUTO, STOPLIST = SYSTEM)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="paging"></param>
|
|
||||||
/// <param name="term"></param>
|
|
||||||
/// <param name="poetId"></param>
|
|
||||||
/// <param name="catId"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<RServiceResult<(PaginationMetadata PagingMeta, GanjoorPoemCompleteViewModel[] Items)>> SearchTajikAsync(PagingParameterModel paging, string term, int? poetId, int? catId)
|
|
||||||
{
|
|
||||||
term = term.Trim();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(term))
|
|
||||||
{
|
|
||||||
return new RServiceResult<(PaginationMetadata PagingMeta, GanjoorPoemCompleteViewModel[] Items)>((null, null), "Лутфан иборатиро ворид кунед");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
string searchConditions;
|
|
||||||
if (term.IndexOf('"') == 0 && term.LastIndexOf('"') == (term.Length - 1))
|
|
||||||
{
|
|
||||||
searchConditions = term.Replace("\"", "").Replace("'", "");
|
|
||||||
searchConditions = $"\"{searchConditions}\"";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string[] words = term.Replace("\"", "").Replace("'", "").Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
searchConditions = "";
|
|
||||||
string emptyOrAnd = "";
|
|
||||||
foreach (string word in words)
|
|
||||||
{
|
|
||||||
searchConditions += $" {emptyOrAnd} \"*{word}*\" ";
|
|
||||||
emptyOrAnd = " AND ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (poetId == null)
|
|
||||||
{
|
|
||||||
catId = null;
|
|
||||||
}
|
|
||||||
if (poetId != null && catId == null)
|
|
||||||
{
|
|
||||||
var poetRes = await GetPoetById((int)poetId);
|
|
||||||
if (!string.IsNullOrEmpty(poetRes.ExceptionString))
|
|
||||||
return new RServiceResult<(PaginationMetadata PagingMeta, GanjoorPoemCompleteViewModel[] Items)>((null, null), poetRes.ExceptionString);
|
|
||||||
catId = poetRes.Result.Cat.Id;
|
|
||||||
}
|
|
||||||
List<int> catIdList = new List<int>();
|
|
||||||
if (catId != null)
|
|
||||||
{
|
|
||||||
catIdList.Add((int)catId);
|
|
||||||
await _populateCategoryChildren(_context, (int)catId, catIdList);
|
|
||||||
}
|
|
||||||
|
|
||||||
var source =
|
|
||||||
_context.TajikPoems
|
|
||||||
.Where(p =>
|
|
||||||
(catId == null || catIdList.Contains(p.CatId))
|
|
||||||
&&
|
|
||||||
EF.Functions.Contains(p.TajikPlainText, searchConditions)
|
|
||||||
)
|
|
||||||
.Include(p => p.Cat).ThenInclude(c => c.Poet)
|
|
||||||
.OrderBy(p => p.Cat.Poet.BirthYearInLHijri).ThenBy(p => p.Cat.Poet.TajikNickname).ThenBy(p => p.Id)
|
|
||||||
.Select
|
|
||||||
(
|
|
||||||
poem =>
|
|
||||||
new GanjoorPoemCompleteViewModel()
|
|
||||||
{
|
|
||||||
Id = poem.Id,
|
|
||||||
Title = poem.TajikTitle,
|
|
||||||
FullTitle = poem.FullTitle,
|
|
||||||
FullUrl = poem.FullUrl,
|
|
||||||
PlainText = poem.TajikPlainText,
|
|
||||||
Category = new GanjoorPoetCompleteViewModel()
|
|
||||||
{
|
|
||||||
Poet = new GanjoorPoetViewModel()
|
|
||||||
{
|
|
||||||
Id = poem.Cat.Poet.Id,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
).AsNoTracking();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(PaginationMetadata PagingMeta, GanjoorPoemCompleteViewModel[] Items) paginatedResult =
|
|
||||||
await QueryablePaginator<GanjoorPoemCompleteViewModel>.Paginate(source, paging);
|
|
||||||
|
|
||||||
|
|
||||||
Dictionary<int, GanjoorPoetCompleteViewModel> cachedPoets = new Dictionary<int, GanjoorPoetCompleteViewModel>();
|
|
||||||
|
|
||||||
foreach (var item in paginatedResult.Items)
|
|
||||||
{
|
|
||||||
if (cachedPoets.TryGetValue(item.Category.Poet.Id, out GanjoorPoetCompleteViewModel poet))
|
|
||||||
{
|
|
||||||
item.Category = poet;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
poet = (await GetPoetById(item.Category.Poet.Id)).Result;
|
|
||||||
|
|
||||||
var tajikPoet = await _context.TajikPoets.AsNoTracking().Where(p => p.Id == item.Category.Poet.Id).SingleAsync();
|
|
||||||
poet.Poet.Nickname = tajikPoet.TajikNickname;
|
|
||||||
|
|
||||||
cachedPoets.Add(item.Category.Poet.Id, poet);
|
|
||||||
|
|
||||||
item.Category = poet;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return new RServiceResult<(PaginationMetadata PagingMeta, GanjoorPoemCompleteViewModel[] Items)>(paginatedResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,359 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using RMuseum.DbContext;
|
|
||||||
using RMuseum.Models.Ganjoor;
|
|
||||||
using RMuseum.Models.Ganjoor.PublicExport;
|
|
||||||
using RMuseum.Utils.PublicDataExport;
|
|
||||||
using RSecurityBackend.Models.Generic;
|
|
||||||
using RSecurityBackend.Services.Implementation;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace RMuseum.Services.Implementation
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// IGanjoorService implementation
|
|
||||||
/// </summary>
|
|
||||||
public partial class GanjoorService : IGanjoorService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Exports the Tajik (Cyrillic transliteration) overlay data — GanjoorTajikPoet/Cat/Poem/Verse
|
|
||||||
/// — to its own git-tracked JSON repository, separate from the main ganjoor-data export.
|
|
||||||
/// Kept separate deliberately: Tajik translation coverage is sparse (most poets/categories/
|
|
||||||
/// poems have no Tajik counterpart at all), the consumer audience is different (Cyrillic
|
|
||||||
/// Tajik apps, not general Ganjoor API consumers), and update cadence is independent of the
|
|
||||||
/// main Persian corpus. This export only ever writes entries that actually have a Tajik
|
|
||||||
/// database record; everything else is skipped. Ids and FullUrls match the main
|
|
||||||
/// ganjoor-data repo exactly, so a consumer resolves structure/metre/sections for a poem by
|
|
||||||
/// fetching the same id/path from there and merging verses by VOrder.
|
|
||||||
/// </summary>
|
|
||||||
public RServiceResult<bool> StartBatchExportTajikPublicGitData()
|
|
||||||
{
|
|
||||||
bool acquiredGuard = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// scans every DTO in RMuseum.Models.Ganjoor.PublicExport, which now includes the
|
|
||||||
// Tajik DTOs too — no separate call needed, they're covered automatically
|
|
||||||
PublicExportSafetyGuard.AssertSafe();
|
|
||||||
|
|
||||||
if (!TryStartExclusiveExportJob("TajikPublicDataExport"))
|
|
||||||
{
|
|
||||||
return new RServiceResult<bool>(false,
|
|
||||||
"A Tajik public data export is already running (check the Jobs page) — wait for it to finish before starting another.");
|
|
||||||
}
|
|
||||||
acquiredGuard = true;
|
|
||||||
|
|
||||||
_backgroundTaskQueue.QueueBackgroundWorkItem
|
|
||||||
(
|
|
||||||
async token =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions<RMuseumDbContext>()))
|
|
||||||
{
|
|
||||||
LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context);
|
|
||||||
var job = (await jobProgressServiceEF.NewJob("TajikPublicDataExport", "Preparing working copy")).Result;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var options = ReadTajikPublicDataExportOptions();
|
|
||||||
var publisher = new GitRepoPublisher(options);
|
|
||||||
publisher.EnsureWorkingCopyUpToDate();
|
|
||||||
|
|
||||||
string repoRoot = options.LocalWorkingCopyPath;
|
|
||||||
|
|
||||||
var manifest = new TajikPublicExportManifestDto
|
|
||||||
{
|
|
||||||
GeneratedAtUtc = DateTime.UtcNow.ToString("O"),
|
|
||||||
IdIndexShardSize = IdIndexShardSize,
|
|
||||||
};
|
|
||||||
|
|
||||||
var poetIdIndex = new Dictionary<int, string>();
|
|
||||||
var catIdIndex = new Dictionary<int, string>();
|
|
||||||
var poemIdIndex = new Dictionary<int, string>();
|
|
||||||
|
|
||||||
var tajikPoets = await context.TajikPoets.AsNoTracking().OrderBy(p => p.Id).ToListAsync();
|
|
||||||
|
|
||||||
int poetIndex = 0;
|
|
||||||
foreach (var tajikPoet in tajikPoets)
|
|
||||||
{
|
|
||||||
poetIndex++;
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, (int)(100.0 * poetIndex / Math.Max(1, tajikPoets.Count)), $"Exporting {tajikPoet.TajikNickname}");
|
|
||||||
|
|
||||||
// the Tajik tables have no FullUrl of their own - poet/category/
|
|
||||||
// poem paths are read from the main tables, since ids match 1:1
|
|
||||||
var catPoet = await context.GanjoorCategories.AsNoTracking()
|
|
||||||
.Where(c => c.PoetId == tajikPoet.Id && c.ParentId == null)
|
|
||||||
.SingleOrDefaultAsync();
|
|
||||||
if (catPoet == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
await ExportTajikPoetToJson(repoRoot, tajikPoet, catPoet);
|
|
||||||
poetIdIndex[tajikPoet.Id] = catPoet.FullUrl;
|
|
||||||
|
|
||||||
int poemCount = await ExportTajikPoetContent(context, repoRoot, tajikPoet.Id, catPoet, catIdIndex, poemIdIndex);
|
|
||||||
manifest.PoemsCount += poemCount;
|
|
||||||
|
|
||||||
manifest.Poets.Add(new PublicExportManifestPoetEntryDto
|
|
||||||
{
|
|
||||||
Id = tajikPoet.Id,
|
|
||||||
Nickname = tajikPoet.TajikNickname,
|
|
||||||
FullUrl = catPoet.FullUrl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest.PoetsCount = manifest.Poets.Count;
|
|
||||||
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 98, "Writing id indexes");
|
|
||||||
await IdIndexWriter.WriteFlatIndexAsync(repoRoot, "index/poets-by-id.json", poetIdIndex);
|
|
||||||
await IdIndexWriter.WriteShardedIndexAsync(repoRoot, "cats", catIdIndex, IdIndexShardSize);
|
|
||||||
await IdIndexWriter.WriteShardedIndexAsync(repoRoot, "poems", poemIdIndex, IdIndexShardSize);
|
|
||||||
|
|
||||||
await DeterministicJsonWriter.WriteIfChangedAsync(Path.Combine(repoRoot, "manifest.json"), manifest);
|
|
||||||
await TextFileWriter.WriteIfChangedAsync(Path.Combine(repoRoot, "API.md"), BuildTajikApiMarkdown(manifest));
|
|
||||||
await TextFileWriter.WriteIfChangedAsync(Path.Combine(repoRoot, "README.md"), BuildTajikReadmeMarkdown(manifest));
|
|
||||||
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 99, "Committing and pushing");
|
|
||||||
int changed = publisher.CommitAndPush($"data: export {manifest.PoetsCount} poets / {manifest.PoemsCount} poems — {DateTime.UtcNow:yyyy-MM-dd}");
|
|
||||||
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 100, changed == 0 ? "No changes" : $"{changed} files changed", true);
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
EndExclusiveExportJob("TajikPublicDataExport");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return new RServiceResult<bool>(true);
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
if (acquiredGuard)
|
|
||||||
{
|
|
||||||
EndExclusiveExportJob("TajikPublicDataExport"); // queueing itself threw after we'd already acquired the guard
|
|
||||||
}
|
|
||||||
return new RServiceResult<bool>(false, exp.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private GitRepoPublisherOptions ReadTajikPublicDataExportOptions()
|
|
||||||
{
|
|
||||||
var section = Configuration.GetSection("TajikPublicDataExport");
|
|
||||||
return new GitRepoPublisherOptions
|
|
||||||
{
|
|
||||||
LocalWorkingCopyPath = section["LocalWorkingCopyPath"],
|
|
||||||
RemoteUrl = section["RemoteUrl"],
|
|
||||||
Branch = section["Branch"] ?? "main",
|
|
||||||
CommitAuthorName = section["CommitAuthorName"] ?? "Ganjoor Tajik Export Bot",
|
|
||||||
CommitAuthorEmail = section["CommitAuthorEmail"] ?? "bot@ganjoor.net",
|
|
||||||
PushEnabled = bool.TryParse(section["PushEnabled"], out var push) && push,
|
|
||||||
GitUserName = section["GitUserName"],
|
|
||||||
GitToken = section["GitToken"],
|
|
||||||
GitExecutablePath = section["GitExecutablePath"],
|
|
||||||
CommandTimeoutMinutes = int.TryParse(section["CommandTimeoutMinutes"], out var timeout) ? timeout : 45,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExportTajikPoetToJson(string repoRoot, GanjoorTajikPoet tajikPoet, GanjoorCat catPoet)
|
|
||||||
{
|
|
||||||
var dto = new TajikPoetPublicDto
|
|
||||||
{
|
|
||||||
Id = tajikPoet.Id,
|
|
||||||
TajikNickname = tajikPoet.TajikNickname,
|
|
||||||
TajikDescription = tajikPoet.TajikDescription,
|
|
||||||
BirthYearInLHijri = tajikPoet.BirthYearInLHijri,
|
|
||||||
FullUrl = catPoet.FullUrl,
|
|
||||||
};
|
|
||||||
|
|
||||||
string path = Path.Combine(repoRoot, "poets", TrimLeadingSlash(catPoet.FullUrl), "poet.json");
|
|
||||||
await DeterministicJsonWriter.WriteIfChangedAsync(path, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// batch-loads everything Tajik for this poet (mirrors the same "load once per poet, not
|
|
||||||
/// once per poem" fix applied to the main export) before walking the tree
|
|
||||||
/// </summary>
|
|
||||||
private async Task<int> ExportTajikPoetContent(RMuseumDbContext context, string repoRoot, int poetId, GanjoorCat catPoet,
|
|
||||||
Dictionary<int, string> catIdIndex, Dictionary<int, string> poemIdIndex)
|
|
||||||
{
|
|
||||||
var tajikCatsById = (await context.TajikCats.AsNoTracking().Where(c => c.PoetId == poetId).ToListAsync())
|
|
||||||
.ToDictionary(c => c.Id);
|
|
||||||
|
|
||||||
if (tajikCatsById.Count == 0)
|
|
||||||
return 0; // nothing translated for this poet at all
|
|
||||||
|
|
||||||
var catIds = tajikCatsById.Keys.ToList();
|
|
||||||
|
|
||||||
var tajikPoemsById = (await context.TajikPoems.AsNoTracking().Where(p => catIds.Contains(p.CatId)).ToListAsync())
|
|
||||||
.ToDictionary(p => p.Id);
|
|
||||||
|
|
||||||
var poemIds = tajikPoemsById.Keys.ToList();
|
|
||||||
|
|
||||||
var tajikVersesByPoem = (await context.TajikVerses.AsNoTracking()
|
|
||||||
.Where(v => poemIds.Contains(v.PoemId))
|
|
||||||
.OrderBy(v => v.VOrder)
|
|
||||||
.ToListAsync())
|
|
||||||
.GroupBy(v => v.PoemId)
|
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
|
||||||
|
|
||||||
return await ExportTajikCatTreeToJson(context, repoRoot, catPoet, tajikCatsById, tajikPoemsById, tajikVersesByPoem, catIdIndex, poemIdIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// walks the *main* category tree (for structure/ordering — Tajik tables have no tree
|
|
||||||
/// shape of their own) but only ever writes a file where a Tajik counterpart actually
|
|
||||||
/// exists; still recurses into every child regardless, since a deeper category can have a
|
|
||||||
/// translation even when its parent doesn't.
|
|
||||||
/// </summary>
|
|
||||||
private async Task<int> ExportTajikCatTreeToJson(RMuseumDbContext context, string repoRoot, GanjoorCat cat,
|
|
||||||
Dictionary<int, GanjoorTajikCat> tajikCatsById, Dictionary<int, GanjoorTajikPoem> tajikPoemsById,
|
|
||||||
Dictionary<int, List<GanjoorTajikVerse>> tajikVersesByPoem,
|
|
||||||
Dictionary<int, string> catIdIndex, Dictionary<int, string> poemIdIndex)
|
|
||||||
{
|
|
||||||
var childCats = await context.GanjoorCategories.AsNoTracking()
|
|
||||||
.Where(c => c.ParentId == cat.Id)
|
|
||||||
.OrderBy(c => c.Id)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
int poemCount = 0;
|
|
||||||
|
|
||||||
if (tajikCatsById.TryGetValue(cat.Id, out var tajikCat))
|
|
||||||
{
|
|
||||||
var poems = await context.GanjoorPoems.AsNoTracking()
|
|
||||||
.Where(p => p.CatId == cat.Id)
|
|
||||||
.OrderBy(p => p.Id)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var translatedChildCats = childCats.Where(c => tajikCatsById.ContainsKey(c.Id)).ToList();
|
|
||||||
var translatedPoems = poems.Where(p => tajikPoemsById.ContainsKey(p.Id)).ToList();
|
|
||||||
|
|
||||||
var catDto = new TajikCatPublicDto
|
|
||||||
{
|
|
||||||
Id = cat.Id,
|
|
||||||
PoetId = cat.PoetId,
|
|
||||||
ParentId = cat.ParentId,
|
|
||||||
TajikTitle = tajikCat.TajikTitle,
|
|
||||||
TajikDescription = tajikCat.TajikDescription,
|
|
||||||
FullUrl = cat.FullUrl,
|
|
||||||
ChildCats = translatedChildCats.Select(c => new CatChildRefDto { Id = c.Id, Title = tajikCatsById[c.Id].TajikTitle, FullUrl = c.FullUrl }).ToList(),
|
|
||||||
Poems = translatedPoems.Select(p => new PoemChildRefDto { Id = p.Id, Title = tajikPoemsById[p.Id].TajikTitle, FullUrl = p.FullUrl }).ToList(),
|
|
||||||
};
|
|
||||||
|
|
||||||
string catDir = Path.Combine(repoRoot, "poets", TrimLeadingSlash(cat.FullUrl));
|
|
||||||
await DeterministicJsonWriter.WriteIfChangedAsync(Path.Combine(catDir, "_cat.json"), catDto);
|
|
||||||
catIdIndex[cat.Id] = cat.FullUrl;
|
|
||||||
|
|
||||||
foreach (var poem in translatedPoems)
|
|
||||||
{
|
|
||||||
var tajikPoem = tajikPoemsById[poem.Id];
|
|
||||||
tajikVersesByPoem.TryGetValue(poem.Id, out var verses);
|
|
||||||
await ExportTajikPoemToJson(repoRoot, poem, tajikPoem, verses ?? new List<GanjoorTajikVerse>());
|
|
||||||
poemIdIndex[poem.Id] = poem.FullUrl;
|
|
||||||
poemCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var childCat in childCats)
|
|
||||||
{
|
|
||||||
poemCount += await ExportTajikCatTreeToJson(context, repoRoot, childCat, tajikCatsById, tajikPoemsById, tajikVersesByPoem, catIdIndex, poemIdIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return poemCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExportTajikPoemToJson(string repoRoot, GanjoorPoem poem, GanjoorTajikPoem tajikPoem, List<GanjoorTajikVerse> verses)
|
|
||||||
{
|
|
||||||
var dto = new TajikPoemPublicDto
|
|
||||||
{
|
|
||||||
Id = poem.Id,
|
|
||||||
CatId = poem.CatId,
|
|
||||||
TajikTitle = tajikPoem.TajikTitle,
|
|
||||||
FullTitle = tajikPoem.FullTitle,
|
|
||||||
FullUrl = poem.FullUrl,
|
|
||||||
TajikPlainText = tajikPoem.TajikPlainText,
|
|
||||||
Verses = verses.OrderBy(v => v.VOrder).Select(v => new TajikVersePublicDto { VOrder = v.VOrder, TajikText = v.TajikText }).ToList(),
|
|
||||||
};
|
|
||||||
|
|
||||||
string path = Path.Combine(repoRoot, "poets", TrimLeadingSlash(poem.FullUrl) + ".json");
|
|
||||||
await DeterministicJsonWriter.WriteIfChangedAsync(path, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildTajikReadmeMarkdown(TajikPublicExportManifestDto manifest)
|
|
||||||
{
|
|
||||||
return
|
|
||||||
$@"# ganjoor-tajik-data
|
|
||||||
|
|
||||||
**[▶ Live demo](https://ganjoor.github.io/tjmini/)** — Мини-Ганҷур, a minimal reading app built
|
|
||||||
on this data, running client-side in your browser with no server of its own.
|
|
||||||
|
|
||||||
Cyrillic Tajik transliteration/translation overlay for [Ganjoor](https://ganjoor.net)'s poetry
|
|
||||||
content. This is a **separate, sparser** data set from
|
|
||||||
[ganjoor-data](https://github.com/ganjoor/ganjoor-data) — Tajik coverage only exists for some
|
|
||||||
poets/categories/poems, not the full corpus, so this repo only ever contains entries that
|
|
||||||
actually have a Tajik translation.
|
|
||||||
|
|
||||||
Ids and paths in this repo match [ganjoor-data]({manifest.MainDataRepo}) exactly (same underlying
|
|
||||||
poet/category/poem). This repo carries only the Tajik title/text; for metre, rhyme, verse
|
|
||||||
structure, and sections, fetch the same id/path from `ganjoor-data` and merge verses by `vOrder`.
|
|
||||||
|
|
||||||
Currently tracks **{manifest.PoetsCount} poets** / **{manifest.PoemsCount} poems** with Tajik
|
|
||||||
content, generated {manifest.GeneratedAtUtc}.
|
|
||||||
|
|
||||||
## Where do I start?
|
|
||||||
|
|
||||||
- **[`manifest.json`](manifest.json)** — the list of poets that have any Tajik content, plus URL
|
|
||||||
templates for every other file kind here.
|
|
||||||
- **[`API.md`](API.md)** — how to fetch any of this over plain HTTPS, and how to cross-reference
|
|
||||||
`ganjoor-data` for structural data this repo doesn't duplicate.
|
|
||||||
";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildTajikApiMarkdown(TajikPublicExportManifestDto manifest)
|
|
||||||
{
|
|
||||||
var t = manifest.UrlTemplates;
|
|
||||||
return
|
|
||||||
$@"# ganjoor-tajik-data — static API
|
|
||||||
|
|
||||||
Same static-API-via-jsDelivr approach as [ganjoor-data]({manifest.MainDataRepo}) — no server,
|
|
||||||
every ""endpoint"" below is a plain HTTPS file fetch:
|
|
||||||
|
|
||||||
https://cdn.jsdelivr.net/gh/ganjoor/ganjoor-tajik-data@main/
|
|
||||||
|
|
||||||
## Content
|
|
||||||
|
|
||||||
- `GET {t.Poet}` — Tajik nickname/description for a poet
|
|
||||||
- `GET {t.Category}` — Tajik title/description for a category, plus only the children that
|
|
||||||
themselves have Tajik content
|
|
||||||
- `GET {t.Poem}` — Tajik title and per-verse text (`vOrder` + `tajikText` only)
|
|
||||||
|
|
||||||
## This repo does not stand alone
|
|
||||||
|
|
||||||
A poem file here has no metre, rhyme, or verse-position/section data — those live in
|
|
||||||
[ganjoor-data]({manifest.MainDataRepo}) under the *same* id and path. To render a Tajik poem
|
|
||||||
fully: fetch this repo's poem file for the Tajik text, fetch `ganjoor-data`'s poem file (same
|
|
||||||
`{t.Poem}` path) for structure, and merge the two verse lists by `vOrder`.
|
|
||||||
|
|
||||||
## Resolving a numeric id
|
|
||||||
|
|
||||||
Same scheme as `ganjoor-data`: `{t.PoetIdIndex}` (flat), `{t.CatIdIndexShard}` /
|
|
||||||
`{t.PoemIdIndexShard}` (bucketed, `bucket = id / {manifest.IdIndexShardSize}`). A missing id here
|
|
||||||
just means that poet/category/poem has no Tajik content yet — check `ganjoor-data`'s own id index
|
|
||||||
to confirm the id is valid at all.
|
|
||||||
|
|
||||||
## Not included
|
|
||||||
|
|
||||||
No comments, bookmarks, reading history, edit/correction history, or any other data linked to a
|
|
||||||
user account — same allowlist-only approach as `ganjoor-data`.
|
|
||||||
";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,238 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System;
|
|
||||||
using RMuseum.Models.Ganjoor;
|
|
||||||
using RSecurityBackend.Models.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using RSecurityBackend.Services.Implementation;
|
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using RMuseum.DbContext;
|
|
||||||
using System.Data;
|
|
||||||
using System.IO;
|
|
||||||
using Dapper;
|
|
||||||
|
|
||||||
namespace RMuseum.Services.Implementation
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// IGanjoorService implementation
|
|
||||||
/// </summary>
|
|
||||||
public partial class GanjoorService : IGanjoorService
|
|
||||||
{
|
|
||||||
public RServiceResult<bool> TajikImportFromSqlite(int poetId, string filePath)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_backgroundTaskQueue.QueueBackgroundWorkItem
|
|
||||||
(
|
|
||||||
async token =>
|
|
||||||
{
|
|
||||||
using (RMuseumDbContext context = new RMuseumDbContext(new DbContextOptions<RMuseumDbContext>())) //this is long running job, so _context might be already been freed/collected by GC
|
|
||||||
{
|
|
||||||
LongRunningJobProgressServiceEF jobProgressServiceEF = new LongRunningJobProgressServiceEF(context);
|
|
||||||
var job = (await jobProgressServiceEF.NewJob("ImportFromSqlite", "Query data")).Result;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
SqliteConnectionStringBuilder connectionStringBuilder = new SqliteConnectionStringBuilder();
|
|
||||||
connectionStringBuilder.DataSource = filePath;
|
|
||||||
using (SqliteConnection sqliteConnection = new SqliteConnection(connectionStringBuilder.ToString()))
|
|
||||||
{
|
|
||||||
await sqliteConnection.OpenAsync();
|
|
||||||
IDbConnection sqlite = sqliteConnection;
|
|
||||||
var poets = (await sqlite.QueryAsync("SELECT * FROM poet ORDER BY id")).ToList();
|
|
||||||
if (poetId != 0 && poets.Count != 1)
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, "poets count in sqlite db is not equal to 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var poet in poets)
|
|
||||||
{
|
|
||||||
int poetId = (int)poet.id;
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, poetId, $"poet: {poetId}");
|
|
||||||
if (await context.GanjoorPoets.AnyAsync(p => p.Id == poetId) == false) continue;
|
|
||||||
var cats = (await sqlite.QueryAsync($"SELECT * FROM cat WHERE poet_id = {poetId} ORDER BY id")).ToList();
|
|
||||||
if(cats.Count == 0) continue;
|
|
||||||
if (await context.TajikPoets.AnyAsync(p => p.Id == poetId) == false)
|
|
||||||
{
|
|
||||||
var tajikPoet = new GanjoorTajikPoet()
|
|
||||||
{
|
|
||||||
Id = poetId,
|
|
||||||
TajikNickname = poet.name,
|
|
||||||
TajikDescription = poet.description,
|
|
||||||
BirthYearInLHijri = (await context.GanjoorPoets.AsNoTracking().Where(p => p.Id == poetId).SingleAsync()).BirthYearInLHijri
|
|
||||||
};
|
|
||||||
context.Add(tajikPoet);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var cat in cats)
|
|
||||||
{
|
|
||||||
int catId = (int)cat.id;
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, poetId, $"poet: {poetId} - cat: {catId}");
|
|
||||||
if (await context.GanjoorCategories.AnyAsync(c => c.Id == catId) == false) continue;
|
|
||||||
if(await context.TajikCats.AnyAsync(c => c.Id == catId) == false)
|
|
||||||
{
|
|
||||||
var tajikCat = new GanjoorTajikCat()
|
|
||||||
{
|
|
||||||
Id = catId,
|
|
||||||
PoetId = poetId,
|
|
||||||
TajikTitle = cat.text
|
|
||||||
};
|
|
||||||
context.Add(tajikCat);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
var poems = (await sqlite.QueryAsync($"SELECT * FROM poem WHERE cat_id = {catId} ORDER BY id")).ToList();
|
|
||||||
foreach (var poem in poems)
|
|
||||||
{
|
|
||||||
int poemId = (int)poem.id;
|
|
||||||
if (await context.GanjoorPoems.AnyAsync(p => p.Id == poemId) == false) continue;
|
|
||||||
if(await context.TajikPoems.AnyAsync(p => p.Id == poemId) == false)
|
|
||||||
{
|
|
||||||
var verses = (await sqlite.QueryAsync($"SELECT * FROM verse WHERE poem_id = {poemId} ORDER BY vorder")).ToList();
|
|
||||||
var ganjoorVerses = await context.GanjoorVerses.AsNoTracking().Where(v => v.PoemId == poemId).ToListAsync();
|
|
||||||
|
|
||||||
if (verses.Count != ganjoorVerses.Count) continue;
|
|
||||||
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, poetId, $"poet: {poetId} - cat: {catId} - poem: {poemId}");
|
|
||||||
int currentCatId = catId;
|
|
||||||
string fullTitle = poem.title;
|
|
||||||
while (currentCatId != 0)
|
|
||||||
{
|
|
||||||
var tajikCat = await context.TajikCats.AsNoTracking().Where(c => c.Id == currentCatId).SingleOrDefaultAsync();
|
|
||||||
|
|
||||||
if(tajikCat == null)
|
|
||||||
{
|
|
||||||
if(null == await context.TajikCats.AsNoTracking().Where(c => c.Id == catId).SingleOrDefaultAsync())
|
|
||||||
{
|
|
||||||
currentCatId = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var gCat = await context.GanjoorCategories.AsNoTracking().Where(c => c.Id == tajikCat.Id).SingleAsync();
|
|
||||||
fullTitle = tajikCat.TajikTitle + " - " + fullTitle;
|
|
||||||
currentCatId = gCat.ParentId ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentCatId == -1)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
|
|
||||||
var ganjoorPoem = await context.GanjoorPoems.AsNoTracking().Where(p => p.Id == poemId).SingleAsync();
|
|
||||||
var tajikPoem = new GanjoorTajikPoem()
|
|
||||||
{
|
|
||||||
Id = poemId,
|
|
||||||
CatId = catId,
|
|
||||||
FullUrl = ganjoorPoem.FullUrl,
|
|
||||||
FullTitle = fullTitle,
|
|
||||||
TajikTitle = poem.title,
|
|
||||||
TajikPlainText = "",
|
|
||||||
};
|
|
||||||
context.Add(tajikPoem);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, poetId, $"poet: {poetId} - cat: {catId} - poem: {poemId} - query verses");
|
|
||||||
string plaintText = "";
|
|
||||||
foreach (var verse in verses)
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, poetId, $"poet: {poetId} - cat: {catId} - poem: {poemId} - verse: {verse.vorder}");
|
|
||||||
var ganjoorVerse = ganjoorVerses.Where(v => v.VOrder == (int)verse.vorder).SingleOrDefault();
|
|
||||||
if (ganjoorVerse == null) continue;
|
|
||||||
var tajikVerse = new GanjoorTajikVerse()
|
|
||||||
{
|
|
||||||
Id = ganjoorVerse.Id,
|
|
||||||
PoemId = poemId,
|
|
||||||
TajikText = verse.text,
|
|
||||||
VOrder = (int)verse.vorder,
|
|
||||||
};
|
|
||||||
context.Add(tajikVerse);
|
|
||||||
plaintText += tajikVerse.TajikText;
|
|
||||||
plaintText += "\r\n";
|
|
||||||
}
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
tajikPoem.TajikPlainText = plaintText;
|
|
||||||
context.Update(tajikPoem);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, poetId, $"poet: {poetId} - cat: {catId} - poem: {poemId} - page");
|
|
||||||
if (await context.TajikPages.AnyAsync(p => p.Id == poemId) == false)
|
|
||||||
{
|
|
||||||
GanjoorTajikPage page = new GanjoorTajikPage()
|
|
||||||
{
|
|
||||||
Id = poemId,
|
|
||||||
TajikHtmlText = PrepareHtmlText
|
|
||||||
(
|
|
||||||
|
|
||||||
verses.Select(s => new GanjoorVerse()
|
|
||||||
{
|
|
||||||
VOrder = (int)s.vorder,
|
|
||||||
Text = s.text,
|
|
||||||
VersePosition = (VersePosition) ((int)s.position)
|
|
||||||
}
|
|
||||||
).ToList()
|
|
||||||
),
|
|
||||||
};
|
|
||||||
context.Add(page);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var cat in cats)
|
|
||||||
{
|
|
||||||
int catId = (int)cat.id;
|
|
||||||
if (await context.GanjoorCategories.AnyAsync(c => c.Id == catId) == false) continue;
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, poetId, $"poet: {poetId} - cat: {catId} - page");
|
|
||||||
var catPage = await context.GanjoorPages.AsNoTracking().Where(p => p.GanjoorPageType == GanjoorPageType.CatPage && p.CatId == catId).SingleOrDefaultAsync();
|
|
||||||
if(catPage == null) continue;
|
|
||||||
if (await context.TajikPages.AnyAsync(p => p.Id == catPage.Id) == false)
|
|
||||||
{
|
|
||||||
var tajikCat = await context.TajikCats.AsNoTracking().Where(c => c.Id == catId).SingleAsync();
|
|
||||||
GanjoorTajikPage page = new GanjoorTajikPage()
|
|
||||||
{
|
|
||||||
Id = catPage.Id,
|
|
||||||
TajikHtmlText = await PrepareTajikCatHtmlTextAsync(context, tajikCat),
|
|
||||||
};
|
|
||||||
context.Add(page);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var poetPage = await context.GanjoorPages.AsNoTracking().Where(p => p.GanjoorPageType == GanjoorPageType.PoetPage && p.PoetId == poetId).SingleAsync();
|
|
||||||
if (await context.TajikPages.AnyAsync(p => p.Id == poetPage.Id) == false)
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, poetId, $"poet: {poetId} - page");
|
|
||||||
var tajikPoet = await context.TajikPoets.AsNoTracking().Where(p => p.Id == poetId).SingleAsync();
|
|
||||||
GanjoorTajikPage page = new GanjoorTajikPage()
|
|
||||||
{
|
|
||||||
Id = poetPage.Id,
|
|
||||||
TajikHtmlText = await PrepareTajikPoetHtmlTextAsync(context, tajikPoet),
|
|
||||||
};
|
|
||||||
context.Add(page);
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true);
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
File.Delete(filePath);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception exp)
|
|
||||||
{
|
|
||||||
return new RServiceResult<bool>(false, exp.ToString());
|
|
||||||
}
|
|
||||||
return new RServiceResult<bool>(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System;
|
|
||||||
using System.Data;
|
|
||||||
using System.Linq;
|
|
||||||
using RMuseum.DbContext;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using RSecurityBackend.Services.Implementation;
|
|
||||||
using RMuseum.Utils;
|
|
||||||
using RMuseum.Models.Ganjoor;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace RMuseum.Services.Implementation
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// IGanjoorService implementation
|
|
||||||
/// </summary>
|
|
||||||
public partial class GanjoorService : IGanjoorService
|
|
||||||
{
|
|
||||||
private async Task<string> PrepareTajikPoetHtmlTextAsync(RMuseumDbContext context, GanjoorTajikPoet poet)
|
|
||||||
{
|
|
||||||
|
|
||||||
string[] lines = poet.TajikDescription.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
string html = "";
|
|
||||||
foreach (var line in lines)
|
|
||||||
{
|
|
||||||
html += $"<p>{line}</p>";
|
|
||||||
}
|
|
||||||
var poetCat = await context.GanjoorCategories.AsNoTracking().Where(c => c.PoetId == poet.Id && c.ParentId == null).SingleAsync();
|
|
||||||
var subCats = await context.GanjoorCategories.AsNoTracking().Where(c => c.ParentId == poetCat.Id).OrderBy(p => p.Id).ToListAsync();
|
|
||||||
foreach (var subCat in subCats)
|
|
||||||
{
|
|
||||||
var tajikCat = await context.TajikCats.AsNoTracking().Where(t => t.Id == subCat.Id).SingleOrDefaultAsync();
|
|
||||||
if(tajikCat != null)
|
|
||||||
{
|
|
||||||
html += $"<p><a href=\"{subCat.FullUrl}\">{LanguageUtils.CleanTextForTransileration(tajikCat.TajikTitle)}</a></p>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var catPoems = await context.GanjoorPoems.AsNoTracking().Where(p => p.CatId == poetCat.Id).OrderBy(p => p.Id).ToListAsync();
|
|
||||||
var tajikPoems = await context.TajikPoems.AsNoTracking().Where(p => p.CatId == poetCat.Id).OrderBy(p => p.Id).ToListAsync();
|
|
||||||
foreach (var catPoem in catPoems)
|
|
||||||
{
|
|
||||||
var tajikPoem = tajikPoems.Where(p => p.Id == catPoem.Id).SingleOrDefault();
|
|
||||||
if(tajikPoem != null)
|
|
||||||
{
|
|
||||||
string excerpt = ExtractExcerpt(tajikPoem.TajikPlainText);
|
|
||||||
string title = LanguageUtils.CleanTextForTransileration(tajikPoem.TajikTitle);
|
|
||||||
html += string.IsNullOrEmpty(excerpt)
|
|
||||||
? $"<p><a href=\"{catPoem.FullUrl}\">{title}</a></p>"
|
|
||||||
: $"<p><a href=\"{catPoem.FullUrl}\">{title}</a>: {excerpt}</p>";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> PrepareTajikCatHtmlTextAsync(RMuseumDbContext context, GanjoorTajikCat cat)
|
|
||||||
{
|
|
||||||
if(cat.TajikDescription == null)
|
|
||||||
{
|
|
||||||
cat.TajikDescription = "";
|
|
||||||
}
|
|
||||||
string[] lines = cat.TajikDescription.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
string html = "";
|
|
||||||
foreach (var line in lines)
|
|
||||||
{
|
|
||||||
html += $"<p>{line}</p>";
|
|
||||||
}
|
|
||||||
var subCats = await context.GanjoorCategories.AsNoTracking().Where(c => c.ParentId == cat.Id).OrderBy(p => p.Id).ToListAsync();
|
|
||||||
foreach (var subCat in subCats)
|
|
||||||
{
|
|
||||||
var tajikCat = await context.TajikCats.AsNoTracking().Where(t => t.Id == subCat.Id).SingleOrDefaultAsync();
|
|
||||||
if (tajikCat != null)
|
|
||||||
{
|
|
||||||
html += $"<p><a href=\"{subCat.FullUrl}\">{LanguageUtils.CleanTextForTransileration(tajikCat.TajikTitle)}</a></p>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var catPoems = await context.GanjoorPoems.AsNoTracking().Where(p => p.CatId == cat.Id).OrderBy(p => p.Id).ToListAsync();
|
|
||||||
var tajikPoems = await context.TajikPoems.AsNoTracking().Where(p => p.CatId == cat.Id).OrderBy(p => p.Id).ToListAsync();
|
|
||||||
foreach (var catPoem in catPoems)
|
|
||||||
{
|
|
||||||
var tajikPoem = tajikPoems.Where(p => p.Id == catPoem.Id).SingleOrDefault();
|
|
||||||
if(tajikPoem != null)
|
|
||||||
{
|
|
||||||
string excerpt = ExtractExcerpt(tajikPoem.TajikPlainText);
|
|
||||||
string title = LanguageUtils.CleanTextForTransileration(tajikPoem.TajikTitle);
|
|
||||||
html += string.IsNullOrEmpty(excerpt)
|
|
||||||
? $"<p><a href=\"{catPoem.FullUrl}\">{title}</a></p>"
|
|
||||||
: $"<p><a href=\"{catPoem.FullUrl}\">{title}</a>: {excerpt}</p>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -92,85 +92,6 @@ namespace RMuseum.Services.Implementation
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// simple, best-effort Perso-Arabic -> Tajik Cyrillic letter substitution.
|
|
||||||
/// NOT a linguistically correct transliteration: Perso-Arabic script does not
|
|
||||||
/// write short vowels, so a letter-by-letter map cannot recover them (that's
|
|
||||||
/// why a dedicated tool like persian-tajik.ir exists, and why the bulk of
|
|
||||||
/// TajikGanjoor's content comes from real TajikPoems/TajikCats/TajikPoets rows
|
|
||||||
/// rather than on-the-fly conversion). This exists only as a fallback for
|
|
||||||
/// spots - like a neighbouring poem's excerpt on the next/previous nav - that
|
|
||||||
/// don't have a proper Tajik translation in the database yet, so at least
|
|
||||||
/// something in Cyrillic is shown instead of raw Perso-Arabic script.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly Dictionary<char, string> _persianToTajikMap = new Dictionary<char, string>()
|
|
||||||
{
|
|
||||||
['آ'] = "о",
|
|
||||||
['ا'] = "о",
|
|
||||||
['ب'] = "б",
|
|
||||||
['پ'] = "п",
|
|
||||||
['ت'] = "т",
|
|
||||||
['ث'] = "с",
|
|
||||||
['ج'] = "ҷ",
|
|
||||||
['چ'] = "ч",
|
|
||||||
['ح'] = "ҳ",
|
|
||||||
['خ'] = "х",
|
|
||||||
['د'] = "д",
|
|
||||||
['ذ'] = "з",
|
|
||||||
['ر'] = "р",
|
|
||||||
['ز'] = "з",
|
|
||||||
['ژ'] = "ж",
|
|
||||||
['س'] = "с",
|
|
||||||
['ش'] = "ш",
|
|
||||||
['ص'] = "с",
|
|
||||||
['ض'] = "з",
|
|
||||||
['ط'] = "т",
|
|
||||||
['ظ'] = "з",
|
|
||||||
['ع'] = "ъ",
|
|
||||||
['غ'] = "ғ",
|
|
||||||
['ف'] = "ф",
|
|
||||||
['ق'] = "қ",
|
|
||||||
['ک'] = "к",
|
|
||||||
['ك'] = "к",
|
|
||||||
['گ'] = "г",
|
|
||||||
['ل'] = "л",
|
|
||||||
['م'] = "м",
|
|
||||||
['ن'] = "н",
|
|
||||||
['و'] = "в",
|
|
||||||
['ه'] = "ҳ",
|
|
||||||
['ی'] = "ӣ",
|
|
||||||
['ي'] = "ӣ",
|
|
||||||
['ئ'] = "ъ",
|
|
||||||
['ؤ'] = "в",
|
|
||||||
['ء'] = "ъ",
|
|
||||||
['ة'] = "ат",
|
|
||||||
['؟'] = "?",
|
|
||||||
['،'] = ",",
|
|
||||||
['؛'] = ";",
|
|
||||||
['۰'] = "0",
|
|
||||||
['۱'] = "1",
|
|
||||||
['۲'] = "2",
|
|
||||||
['۳'] = "3",
|
|
||||||
['۴'] = "4",
|
|
||||||
['۵'] = "5",
|
|
||||||
['۶'] = "6",
|
|
||||||
['۷'] = "7",
|
|
||||||
['۸'] = "8",
|
|
||||||
['۹'] = "9",
|
|
||||||
};
|
|
||||||
|
|
||||||
public static string TransliteratePersianToTajik(string text)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(text)) return text;
|
|
||||||
text = CleanTextForTransileration(text);
|
|
||||||
var sb = new System.Text.StringBuilder(text.Length);
|
|
||||||
foreach (char c in text)
|
|
||||||
{
|
|
||||||
sb.Append(_persianToTajikMap.TryGetValue(c, out string mapped) ? mapped : c.ToString());
|
|
||||||
}
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static string SameSoundLetters(string text)
|
public static string SameSoundLetters(string text)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -68,8 +68,7 @@
|
|||||||
"BooksCoverTitleFontName": "IranNastaliq",
|
"BooksCoverTitleFontName": "IranNastaliq",
|
||||||
"BooksCoverTitleFontSize": "40",
|
"BooksCoverTitleFontSize": "40",
|
||||||
"ImportCommentsAsPoemSummary": "True",
|
"ImportCommentsAsPoemSummary": "True",
|
||||||
"MaxWordLengthInComments": 200,
|
"MaxWordLengthInComments": 200
|
||||||
"TajikSitemapLocation": "C:\\inetpub\\tj\\wwwroot\\sitemap.xml"
|
|
||||||
},
|
},
|
||||||
"PublicDataExport": {
|
"PublicDataExport": {
|
||||||
"LocalWorkingCopyPath": "C:\\ganjoor-public-data",
|
"LocalWorkingCopyPath": "C:\\ganjoor-public-data",
|
||||||
@ -83,18 +82,6 @@
|
|||||||
"GitExecutablePath": "",
|
"GitExecutablePath": "",
|
||||||
"CommandTimeoutMinutes": "45"
|
"CommandTimeoutMinutes": "45"
|
||||||
},
|
},
|
||||||
"TajikPublicDataExport": {
|
|
||||||
"LocalWorkingCopyPath": "C:\\ganjoor-tajik-public-data",
|
|
||||||
"RemoteUrl": "https://github.com/ganjoor/ganjoor-tajik-data.git",
|
|
||||||
"Branch": "main",
|
|
||||||
"CommitAuthorName": "Ganjoor Tajik Export Bot",
|
|
||||||
"CommitAuthorEmail": "bot@ganjoor.net",
|
|
||||||
"PushEnabled": "False",
|
|
||||||
"GitUserName": "",
|
|
||||||
"GitToken": "",
|
|
||||||
"GitExecutablePath": "",
|
|
||||||
"CommandTimeoutMinutes": "45"
|
|
||||||
},
|
|
||||||
"ExternalFTPServer": {
|
"ExternalFTPServer": {
|
||||||
"Host": "localhost",
|
"Host": "localhost",
|
||||||
"Port": "22",
|
"Port": "22",
|
||||||
|
|||||||
@ -1,46 +0,0 @@
|
|||||||
namespace TajikGanjoor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// API Root
|
|
||||||
/// </summary>
|
|
||||||
public static class APIRoot
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// url
|
|
||||||
/// </summary>
|
|
||||||
public static string Url
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_url))
|
|
||||||
return _url;
|
|
||||||
IConfigurationRoot configuration = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json")
|
|
||||||
.Build();
|
|
||||||
_url = configuration["APIRoot"] ?? "https://api.ganjoor.net";
|
|
||||||
return _url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string _url = "";
|
|
||||||
|
|
||||||
private static string _InternetUrl = "";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// internet accessible end point
|
|
||||||
/// </summary>
|
|
||||||
public static string InternetUrl
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_InternetUrl))
|
|
||||||
return _InternetUrl;
|
|
||||||
IConfigurationRoot configuration = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json")
|
|
||||||
.Build();
|
|
||||||
_InternetUrl = configuration["GlobalAPIRoot"] ?? "https://api.ganjoor.net";
|
|
||||||
return _InternetUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
@page
|
|
||||||
@model ErrorModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Error";
|
|
||||||
}
|
|
||||||
|
|
||||||
<h1 class="text-danger">Error.</h1>
|
|
||||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
|
||||||
|
|
||||||
@if (Model.ShowRequestId)
|
|
||||||
{
|
|
||||||
<p>
|
|
||||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
<h3>Development Mode</h3>
|
|
||||||
<p>
|
|
||||||
Swapping to the <strong>Development</strong> environment displays 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>
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace TajikGanjoor.Pages
|
|
||||||
{
|
|
||||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
|
||||||
[IgnoreAntiforgeryToken]
|
|
||||||
public class ErrorModel : PageModel
|
|
||||||
{
|
|
||||||
public string? RequestId { get; set; }
|
|
||||||
|
|
||||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
|
||||||
|
|
||||||
private readonly ILogger<ErrorModel> _logger;
|
|
||||||
|
|
||||||
public ErrorModel(ILogger<ErrorModel> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnGet()
|
|
||||||
{
|
|
||||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,215 +0,0 @@
|
|||||||
@page
|
|
||||||
@model IndexModel
|
|
||||||
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(Model.LastError))
|
|
||||||
{
|
|
||||||
@Html.Raw(Model.LastError)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (Model.Poets == null)
|
|
||||||
{
|
|
||||||
<p>
|
|
||||||
Хато дар дарёфти феҳристи суханварон
|
|
||||||
</p>
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="text-center">
|
|
||||||
@if (Model.IsSearchPage)
|
|
||||||
{
|
|
||||||
@if (Model.Poems == null)
|
|
||||||
{
|
|
||||||
<div class="poem">
|
|
||||||
<h2>Ёфт менашавад ҷустаем мо</h2>
|
|
||||||
<p>
|
|
||||||
Шеъре шомили ин иборат ёфт нашуд.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
for (int poemIndex = 0; poemIndex < Model.Poems.Count; poemIndex++)
|
|
||||||
{
|
|
||||||
var poem = Model.Poems[poemIndex];
|
|
||||||
|
|
||||||
<div class="sitem">
|
|
||||||
<a class="sitem-poet-portrait" href="@poem.Category.Poet.FullUrl" title="@poem.Category.Poet.Nickname">
|
|
||||||
<img src="/images/poets/@(poem.Category.Poet.Id).png" alt="@poem.Category.Poet.Nickname" loading="lazy"
|
|
||||||
onerror="this.onerror=null;this.src='/images/poets/999.png';" />
|
|
||||||
</a>
|
|
||||||
<div class="sitem-body">
|
|
||||||
<h2>
|
|
||||||
<a href="@poem.FullUrl">@poem.FullTitle</a>
|
|
||||||
</h2>
|
|
||||||
<div class="sit">
|
|
||||||
@Html.Raw(poem.PlainText)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="spacer"> </div>
|
|
||||||
|
|
||||||
@Html.Raw(Model.PagingToolsHtml)
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
@if (Model.IsHomePage)
|
|
||||||
{
|
|
||||||
var popularGroup = Model.PoetGroups.SingleOrDefault(g => g.Id == 0);
|
|
||||||
var centuryGroups = Model.PoetGroups.Where(g => g.Id != 0).ToList();
|
|
||||||
|
|
||||||
<div class="tg-poet-search">
|
|
||||||
<input type="text" id="tg-findpoet" placeholder="Ҷустуҷӯи шоир" oninput="tgInlineSearch(this.value)" />
|
|
||||||
<div class="tg-poet-grid" id="tg-found-poets"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tg-view-switch">
|
|
||||||
<button type="button" class="tg-view-switch-btn active" data-view="century" onclick="tgSwitchView('century')">Аз рӯи аср</button>
|
|
||||||
<button type="button" class="tg-view-switch-btn" data-view="alpha" onclick="tgSwitchView('alpha')">Аз рӯи алифбо</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tg-view-century">
|
|
||||||
@if (popularGroup != null)
|
|
||||||
{
|
|
||||||
<div class="tg-century-heading">Шоирони маъмул</div>
|
|
||||||
<div class="tg-poet-grid">
|
|
||||||
@foreach (var poet in popularGroup.Poets)
|
|
||||||
{
|
|
||||||
<a class="tg-poet-card" href="@poet.FullUrl" title="@poet.Name" data-poet-id="@poet.Id" data-value="@poet.Nickname">
|
|
||||||
<span class="tg-poet-portrait">
|
|
||||||
<img src="/images/poets/@(poet.Id).png" alt="@poet.Nickname" loading="lazy"
|
|
||||||
onerror="this.onerror=null;this.src='/images/poets/999.png';" />
|
|
||||||
</span>
|
|
||||||
<span class="tg-poet-name">@poet.Nickname</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (centuryGroups.Count > 0)
|
|
||||||
{
|
|
||||||
<div class="tg-century-heading">Гурӯҳбандӣ бар асоси аср</div>
|
|
||||||
<div class="tg-century-nav">
|
|
||||||
@foreach (var g in centuryGroups)
|
|
||||||
{
|
|
||||||
<a href="#tg-pg-@g.Id" class="circled-number">@(g.HalfCenturyOrder + 2)</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@foreach (var century in centuryGroups)
|
|
||||||
{
|
|
||||||
@if (century.ShowInTimeLine)
|
|
||||||
{
|
|
||||||
<div class="tg-century-heading" id="tg-pg-@century.Id">@century.Name</div>
|
|
||||||
}
|
|
||||||
<div class="tg-poet-grid">
|
|
||||||
@foreach (var poet in century.Poets)
|
|
||||||
{
|
|
||||||
<a class="tg-poet-card" href="@poet.FullUrl" title="@poet.Name" data-poet-id="@poet.Id" data-value="@poet.Nickname">
|
|
||||||
<span class="tg-poet-portrait">
|
|
||||||
<img src="/images/poets/@(poet.Id).png" alt="@poet.Nickname" loading="lazy"
|
|
||||||
onerror="this.onerror=null;this.src='/images/poets/999.png';" />
|
|
||||||
</span>
|
|
||||||
<span class="tg-poet-name">@poet.Nickname</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tg-view-alpha" style="display:none">
|
|
||||||
<div class="tg-century-heading">Аз рӯи алифбо</div>
|
|
||||||
<div class="tg-poet-grid">
|
|
||||||
@foreach (var poet in Model.AlphabeticalPoets)
|
|
||||||
{
|
|
||||||
<a class="tg-poet-card" href="@poet.FullUrl" title="@poet.Name" data-poet-id="@poet.Id" data-value="@poet.Nickname">
|
|
||||||
<span class="tg-poet-portrait">
|
|
||||||
<img src="/images/poets/@(poet.Id).png" alt="@poet.Nickname" loading="lazy"
|
|
||||||
onerror="this.onerror=null;this.src='/images/poets/999.png';" />
|
|
||||||
</span>
|
|
||||||
<span class="tg-poet-name">@poet.Nickname</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@if (Model.IsPoetPage)
|
|
||||||
{
|
|
||||||
<div class="tg-poet-header">
|
|
||||||
<span class="tg-poet-portrait-lg">
|
|
||||||
<img src="/images/poets/@(Model.GanjoorPage.PoetOrCat.Poet.Id).png" alt="@Model.GanjoorPage.PoetOrCat.Poet.Nickname" loading="lazy"
|
|
||||||
onerror="this.onerror=null;this.src='/images/poets/999.png';" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="tg-poet-page">
|
|
||||||
@Html.Raw(Model.GanjoorPage.HtmlText)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (Model.IsCatPage)
|
|
||||||
{
|
|
||||||
<div class="tg-cat-page">
|
|
||||||
@Html.Raw(Model.GanjoorPage.HtmlText)
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else if (Model.IsPoemPage)
|
|
||||||
{
|
|
||||||
@Html.Raw(Model.GanjoorPage.HtmlText)
|
|
||||||
|
|
||||||
@if (Model.GanjoorPage.Poem.Recitations != null && Model.GanjoorPage.Poem.Recitations.Length > 0)
|
|
||||||
{
|
|
||||||
<div class="tg-recitations">
|
|
||||||
<div class="tg-recitations-title">Хониш</div>
|
|
||||||
@foreach (var recitation in Model.GanjoorPage.Poem.Recitations)
|
|
||||||
{
|
|
||||||
<div class="tg-recitation">
|
|
||||||
<audio id="tg-audio-@recitation.Id" controls preload="none">
|
|
||||||
<source src="@recitation.Mp3Url" />
|
|
||||||
</audio>
|
|
||||||
<div class="tg-recitation-caption">
|
|
||||||
<span class="tg-recitation-title">@recitation.AudioTitle</span>
|
|
||||||
@if (!string.IsNullOrEmpty(recitation.AudioArtist))
|
|
||||||
{
|
|
||||||
<span class="tg-recitation-artist">— @recitation.AudioArtist</span>
|
|
||||||
}
|
|
||||||
<a class="tg-recitation-download" href="@recitation.Mp3Url" download title="Боргирӣ">⬇</a>
|
|
||||||
</div>
|
|
||||||
@if (recitation.InSyncWithText)
|
|
||||||
{
|
|
||||||
<script>
|
|
||||||
tgInitRecitationSync(
|
|
||||||
document.getElementById('tg-audio-@recitation.Id'),
|
|
||||||
'@Html.Raw($"{TajikGanjoor.APIRoot.InternetUrl}/api/audio/file/{recitation.Id}.xml")',
|
|
||||||
'@Html.Raw(recitation.AudioArtist)',
|
|
||||||
@recitation.Id
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tg-sticky-player" class="tg-sticky-player" style="display:none;">
|
|
||||||
<span id="tg-sticky-narrator" class="tg-sticky-player-narrator"></span>
|
|
||||||
<div id="tg-sticky-audio-slot" class="tg-sticky-player-audio-slot"></div>
|
|
||||||
<a role="button" id="tg-sticky-lock" class="tg-sticky-player-btn" onclick="tgSwitchScrollLock()" title="Қулфи ҳамоҳангӣ бо матн"><span id="tg-sticky-lock-icon"></span></a>
|
|
||||||
<a role="button" class="tg-sticky-player-btn" onclick="tgScrollToCurrentVerse()" title="Гузариш ба байти ҷорӣ">
|
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 8a4 4 0 100 8 4 4 0 000-8m8.94 3A9 9 0 0013 3.06V1h-2v2.06A9 9 0 003.06 11H1v2h2.06A9 9 0 0011 20.94V23h2v-2.06A9 9 0 0020.94 13H23v-2zM12 19a7 7 0 110-14 7 7 0 010 14z"/></svg>
|
|
||||||
</a>
|
|
||||||
<a role="button" class="tg-sticky-player-btn" onclick="tgCloseStickyPlayer()" title="Пӯшидан">
|
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@Html.Raw(Model.GanjoorPage.HtmlText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@ -1,553 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using RMuseum.Models.Ganjoor.ViewModels;
|
|
||||||
using RMuseum.Models.Ganjoor;
|
|
||||||
using System.Net;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using RSecurityBackend.Models.Generic;
|
|
||||||
using RMuseum.Services.Implementation;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
namespace TajikGanjoor.Pages
|
|
||||||
{
|
|
||||||
public class IndexModel : PageModel
|
|
||||||
{
|
|
||||||
public bool IsHomePage { get; set; }
|
|
||||||
public bool IsSearchPage { get; set; }
|
|
||||||
public List<GanjoorPoetViewModel>? Poets { get; set; }
|
|
||||||
public bool IsPoetPage { get; set; }
|
|
||||||
public bool IsCatPage { get; set; }
|
|
||||||
public bool IsPoemPage { get; set; }
|
|
||||||
public GanjoorPageCompleteViewModel? GanjoorPage { get; set; }
|
|
||||||
public string NextUrl { get; set; }
|
|
||||||
public string NextTitle { get; set; }
|
|
||||||
public string PreviousUrl { get; set; }
|
|
||||||
public string PreviousTitle { get; set; }
|
|
||||||
public string BreadCrumpUrls { get; set; }
|
|
||||||
public string Query { get; set; }
|
|
||||||
public PaginationMetadata PaginationMetadata { get; set; }
|
|
||||||
public string PagingToolsHtml { get; set; }
|
|
||||||
|
|
||||||
public List<GanjoorPoemCompleteViewModel> Poems { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// poets grouped by century, shown on the home page - mirrors GanjooRazor's Index.cshtml.cs
|
|
||||||
/// </summary>
|
|
||||||
public List<GanjoorCenturyViewModel> PoetGroups { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// all translated poets, sorted alphabetically by Tajik nickname - alternate flat view,
|
|
||||||
/// switched to on the home page instead of the century grouping
|
|
||||||
/// </summary>
|
|
||||||
public List<GanjoorPoetViewModel> AlphabeticalPoets { get; set; }
|
|
||||||
|
|
||||||
public async Task<IActionResult> OnGetAsync()
|
|
||||||
{
|
|
||||||
if (bool.Parse(Configuration["MaintenanceMode"] ?? false.ToString()))
|
|
||||||
{
|
|
||||||
return StatusCode(503);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (false == await PreparePoetsAsync())
|
|
||||||
{
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
IsSearchPage = !string.IsNullOrEmpty(Request.Query["s"]);
|
|
||||||
IsHomePage = !IsSearchPage && Request.Path == "/";
|
|
||||||
if (IsHomePage)
|
|
||||||
{
|
|
||||||
if (false == await PreparePoetGroupsAsync())
|
|
||||||
{
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
// plain ordinal sort gets this wrong: the six Tajik-specific letters
|
|
||||||
// (ғ ӣ қ ӯ ҳ ҷ) sit outside the main Cyrillic code point block, so they'd
|
|
||||||
// sort out of alphabetical order (e.g. after "я") under Ordinal - a
|
|
||||||
// culture-aware comparer places them correctly relative to their base
|
|
||||||
// letter (ғ after г, etc). Falls back to Ordinal if "tg-TJ" collation
|
|
||||||
// data isn't available on the server, rather than failing the page.
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var tajikCulture = System.Globalization.CultureInfo.GetCultureInfo("tg-TJ");
|
|
||||||
AlphabeticalPoets = Poets.OrderBy(p => p.Nickname, StringComparer.Create(tajikCulture, false)).ToList();
|
|
||||||
}
|
|
||||||
catch (System.Globalization.CultureNotFoundException)
|
|
||||||
{
|
|
||||||
AlphabeticalPoets = Poets.OrderBy(p => p.Nickname, StringComparer.Ordinal).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int pageNumber = 1;
|
|
||||||
if (IsSearchPage)
|
|
||||||
{
|
|
||||||
Query = Request.Query["s"].ToString().Trim();
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Request.Query["page"]))
|
|
||||||
{
|
|
||||||
pageNumber = int.Parse(Request.Query["page"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpResponseMessage searchQueryResponse = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/tajik/search?term={Query}&PageNumber={pageNumber}&PageSize=20");
|
|
||||||
|
|
||||||
if (!searchQueryResponse.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
LastError = JsonConvert.DeserializeObject<string>(await searchQueryResponse.Content.ReadAsStringAsync());
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
|
|
||||||
Poems = JArray.Parse(await searchQueryResponse.Content.ReadAsStringAsync()).ToObject<List<GanjoorPoemCompleteViewModel>>();
|
|
||||||
if (Poems != null && Poems.Count == 0)
|
|
||||||
{
|
|
||||||
Poems = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Poems != null)
|
|
||||||
{
|
|
||||||
// highlight searched word
|
|
||||||
string[] queryParts = Query.IndexOf('"') == 0 && Query.LastIndexOf('"') == (Query.Length - 1) ?
|
|
||||||
[Query.Replace("\"", "")]
|
|
||||||
:
|
|
||||||
Query.Replace("\"", "").Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
foreach (var poem in Poems)
|
|
||||||
{
|
|
||||||
string[] lines = poem.PlainText.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
List<int> linesInExcerpt = new List<int>();
|
|
||||||
for (int i = 0; i < lines.Length; i++)
|
|
||||||
{
|
|
||||||
foreach (var queryPart in queryParts)
|
|
||||||
{
|
|
||||||
if (lines[i].IndexOf(queryPart) != -1)
|
|
||||||
{
|
|
||||||
if (i > 0)
|
|
||||||
{
|
|
||||||
if (linesInExcerpt.IndexOf(i - 1) == -1)
|
|
||||||
{
|
|
||||||
linesInExcerpt.Add(i - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (linesInExcerpt.IndexOf(i) == -1)
|
|
||||||
{
|
|
||||||
linesInExcerpt.Add(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < (lines.Length - 1))
|
|
||||||
linesInExcerpt.Add(i + 1);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
string plainText = "";
|
|
||||||
for (int i = 0; i < linesInExcerpt.Count; i++)
|
|
||||||
{
|
|
||||||
if (linesInExcerpt[i] > 0 && linesInExcerpt.IndexOf(linesInExcerpt[i] - 1) == -1)
|
|
||||||
plainText += "... ";
|
|
||||||
plainText += $"{lines[linesInExcerpt[i]]}";
|
|
||||||
if (linesInExcerpt[i] < (lines.Length - 1) && linesInExcerpt.IndexOf(linesInExcerpt[i] + 1) == -1)
|
|
||||||
plainText += " ...";
|
|
||||||
plainText += $"{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
|
|
||||||
string finalPlainText = "";
|
|
||||||
foreach (string line in plainText.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries))
|
|
||||||
{
|
|
||||||
finalPlainText += $"<p>{line}</p>";
|
|
||||||
}
|
|
||||||
if (linesInExcerpt.Count > 0)
|
|
||||||
{
|
|
||||||
poem.PlainText = finalPlainText;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < queryParts.Length; i++)
|
|
||||||
{
|
|
||||||
string cssClass = i % 3 == 0 ? "hilite" : i % 3 == 1 ? "hilite2" : "hilite3";
|
|
||||||
poem.PlainText = Regex.Replace(poem.PlainText, queryParts[i], $"<span class=\"{cssClass}\">{queryParts[i]}</span>", RegexOptions.IgnoreCase | RegexOptions.RightToLeft); ;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
string paginationMetadataJsonValue = searchQueryResponse.Headers.GetValues("paging-headers").FirstOrDefault();
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(paginationMetadataJsonValue))
|
|
||||||
{
|
|
||||||
PaginationMetadata = JsonConvert.DeserializeObject<PaginationMetadata>(paginationMetadataJsonValue);
|
|
||||||
string catQuery = "";
|
|
||||||
if (!string.IsNullOrEmpty(Request.Query["cat"]))
|
|
||||||
{
|
|
||||||
catQuery = $"&cat={Request.Query["cat"]}";
|
|
||||||
}
|
|
||||||
PagingToolsHtml = GeneratePagingBarHtml(PaginationMetadata, $"/?s={WebUtility.UrlEncode(Query)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (!IsHomePage)
|
|
||||||
{
|
|
||||||
var pageQuery = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/tajik/page?url={Request.Path}&catPoems=true");
|
|
||||||
if (!pageQuery.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
if (pageQuery.StatusCode == HttpStatusCode.NotFound)
|
|
||||||
{
|
|
||||||
var redirectQuery = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/redirecturl?url={Request.Path}");
|
|
||||||
if (redirectQuery.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var redirectUrl = JsonConvert.DeserializeObject<string>(await redirectQuery.Content.ReadAsStringAsync());
|
|
||||||
return Redirect(redirectUrl ?? "/");
|
|
||||||
}
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!pageQuery.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
LastError = JsonConvert.DeserializeObject<string>(await pageQuery.Content.ReadAsStringAsync());
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
GanjoorPage = JObject.Parse(await pageQuery.Content.ReadAsStringAsync()).ToObject<GanjoorPageCompleteViewModel>();
|
|
||||||
if (GanjoorPage == null)
|
|
||||||
{
|
|
||||||
LastError = "GanjoorPage == null";
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
switch (GanjoorPage.GanjoorPageType)
|
|
||||||
{
|
|
||||||
case GanjoorPageType.PoemPage:
|
|
||||||
GanjoorPage.PoetOrCat = GanjoorPage.Poem.Category;
|
|
||||||
PrepareNextPre();
|
|
||||||
IsPoemPage = true;
|
|
||||||
break;
|
|
||||||
case GanjoorPageType.PoetPage:
|
|
||||||
IsPoetPage = true;
|
|
||||||
break;
|
|
||||||
case GanjoorPageType.CatPage:
|
|
||||||
PrepareNextPre();
|
|
||||||
IsCatPage = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
BreadCrumpUrls = "<a href=\"/\">Ганҷур</a>";
|
|
||||||
if (IsSearchPage)
|
|
||||||
{
|
|
||||||
ViewData["Title"] = $"Ганҷур - ҷустуҷӯ - {Query}";
|
|
||||||
if(pageNumber > 1)
|
|
||||||
{
|
|
||||||
ViewData["Title"] += "Сафҳаи " + pageNumber.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (IsHomePage)
|
|
||||||
{
|
|
||||||
ViewData["Title"] = "Ганҷур";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (IsPoetPage)
|
|
||||||
{
|
|
||||||
ViewData["Title"] = $"Ганҷур - {GanjoorPage?.PoetOrCat.Poet.Nickname}";
|
|
||||||
BreadCrumpUrls += $" - <a href=\"{GanjoorPage?.PoetOrCat.Poet.FullUrl}\" class=\"tg-crumb-poet\"><img src=\"/images/poets/{GanjoorPage?.PoetOrCat.Poet.Id}.png\" alt=\"\" onerror=\"this.onerror=null;this.src='/images/poets/999.png';\" />{GanjoorPage?.PoetOrCat.Poet.Nickname}</a>";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (IsCatPage || IsPoemPage)
|
|
||||||
{
|
|
||||||
string title = $"Ганҷур - ";
|
|
||||||
bool firstAncestor = true;
|
|
||||||
foreach (var gran in GanjoorPage.PoetOrCat.Cat.Ancestors)
|
|
||||||
{
|
|
||||||
title += $"{gran.Title} - ";
|
|
||||||
if (firstAncestor)
|
|
||||||
{
|
|
||||||
// Ancestors[0] is always the poet's own root category (see GanjoorService._GetCatById,
|
|
||||||
// which walks Parent all the way up) - prefix it with the poet's portrait instead of
|
|
||||||
// adding a separate crumb, so the image and the poet's name stay a single link.
|
|
||||||
BreadCrumpUrls += $" - <a href=\"{gran.FullUrl}\" class=\"tg-crumb-poet\"><img src=\"/images/poets/{GanjoorPage.PoetOrCat.Poet.Id}.png\" alt=\"\" onerror=\"this.onerror=null;this.src='/images/poets/999.png';\" />{gran.Title}</a>";
|
|
||||||
firstAncestor = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
BreadCrumpUrls += $" - <a href=\"{gran.FullUrl}\">{gran.Title}</a>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
title += GanjoorPage.PoetOrCat.Cat.Title;
|
|
||||||
BreadCrumpUrls += $" - <a href=\"{GanjoorPage?.PoetOrCat.Cat.FullUrl}\">{GanjoorPage?.PoetOrCat.Cat.Title}</a>";
|
|
||||||
if (IsPoemPage)
|
|
||||||
{
|
|
||||||
title += GanjoorPage.Poem.Title;
|
|
||||||
BreadCrumpUrls += $" - <a href=\"{GanjoorPage?.FullUrl}\">{GanjoorPage?.Poem.Title}</a>";
|
|
||||||
}
|
|
||||||
ViewData["Title"] = title;
|
|
||||||
}
|
|
||||||
ViewData["BreadCrumpUrls"] = BreadCrumpUrls;
|
|
||||||
ViewData["TrackingScript"] = Configuration["TrackingScript"] != null && string.IsNullOrEmpty(Request.Cookies["Token"]) ? Configuration["TrackingScript"].Replace("loggedon", "") : Configuration["TrackingScript"];
|
|
||||||
ViewData["GoogleTranslateLink"] = $"https://translate.google.com/translate?hl=fa&sl=fa&tl=tg&u={Uri.EscapeDataString(stringToEscape: $"https://ganjoor.net{Request.Path}")}";
|
|
||||||
ViewData["NextUrl"] = NextUrl;
|
|
||||||
ViewData["NextTitle"] = NextTitle;
|
|
||||||
ViewData["PreviousUrl"] = PreviousUrl;
|
|
||||||
ViewData["PreviousTitle"] = PreviousTitle;
|
|
||||||
return Page();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> PreparePoetsAsync()
|
|
||||||
{
|
|
||||||
var cacheKey = $"/api/ganjoor/poets";
|
|
||||||
if (!_memoryCache.TryGetValue(cacheKey, out List<GanjoorPoetViewModel>? poets))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var res1 = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/poets");
|
|
||||||
if (!res1.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
LastError = JsonConvert.DeserializeObject<string>(await res1.Content.ReadAsStringAsync()) ?? "!res1.IsSuccessStatusCode";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var ganjoorPoets = JArray.Parse(await res1.Content.ReadAsStringAsync()).ToObject<List<GanjoorPoetViewModel>>();
|
|
||||||
ganjoorPoets = ganjoorPoets ?? [];
|
|
||||||
|
|
||||||
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/tajik/poets");
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
LastError = JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync()) ?? "!response.IsSuccessStatusCode";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var tajkPoets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject<List<GanjoorTajikPoet>>();
|
|
||||||
|
|
||||||
poets = new List<GanjoorPoetViewModel>();
|
|
||||||
foreach (var tajikPoet in tajkPoets)
|
|
||||||
{
|
|
||||||
var poet = ganjoorPoets.Where(p => p.Id == tajikPoet.Id).Single();
|
|
||||||
poet.Nickname = tajikPoet.TajikNickname;
|
|
||||||
poet.Description = tajikPoet.TajikDescription;
|
|
||||||
poets.Add(poet);
|
|
||||||
|
|
||||||
}
|
|
||||||
Poets = poets;
|
|
||||||
|
|
||||||
if (AggressiveCacheEnabled)
|
|
||||||
{
|
|
||||||
_memoryCache.Set(cacheKey, poets, TimeSpan.FromHours(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LastError = e.ToString();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Poets = poets ?? [];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// fetches the same /api/ganjoor/centuries grouping GanjooRazor's homepage uses, then
|
|
||||||
/// overlays Tajik nickname/description onto each group's poets the same way
|
|
||||||
/// PreparePoetsAsync does, dropping poets with no Tajik translation and dropping any
|
|
||||||
/// century that ends up empty as a result - century Name itself has no dedicated Tajik
|
|
||||||
/// translation source anywhere, so it falls back to LanguageUtils.TransliteratePersianToTajik
|
|
||||||
/// (a handful of these are short/fixed enough that hand-correcting the wording later,
|
|
||||||
/// directly in this list, would be easy if the transliteration reads awkwardly)
|
|
||||||
/// </summary>
|
|
||||||
private async Task<bool> PreparePoetGroupsAsync()
|
|
||||||
{
|
|
||||||
var cacheKey = $"/api/ganjoor/tajik/centuries";
|
|
||||||
if (!_memoryCache.TryGetValue(cacheKey, out List<GanjoorCenturyViewModel>? poetGroups))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var res1 = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/centuries");
|
|
||||||
if (!res1.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
LastError = JsonConvert.DeserializeObject<string>(await res1.Content.ReadAsStringAsync()) ?? "!res1.IsSuccessStatusCode";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var centuries = JArray.Parse(await res1.Content.ReadAsStringAsync()).ToObject<List<GanjoorCenturyViewModel>>();
|
|
||||||
centuries = centuries ?? [];
|
|
||||||
|
|
||||||
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/tajik/poets");
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
LastError = JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync()) ?? "!response.IsSuccessStatusCode";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var tajikPoets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject<List<GanjoorTajikPoet>>() ?? [];
|
|
||||||
|
|
||||||
poetGroups = new List<GanjoorCenturyViewModel>();
|
|
||||||
foreach (var century in centuries)
|
|
||||||
{
|
|
||||||
var translatedPoets = new List<GanjoorPoetViewModel>();
|
|
||||||
foreach (var poet in century.Poets)
|
|
||||||
{
|
|
||||||
var tajikPoet = tajikPoets.SingleOrDefault(p => p.Id == poet.Id);
|
|
||||||
if (tajikPoet != null)
|
|
||||||
{
|
|
||||||
poet.Nickname = tajikPoet.TajikNickname;
|
|
||||||
poet.Description = tajikPoet.TajikDescription;
|
|
||||||
translatedPoets.Add(poet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (translatedPoets.Count > 0)
|
|
||||||
{
|
|
||||||
century.Poets = translatedPoets;
|
|
||||||
century.Name = LanguageUtils.TransliteratePersianToTajik(century.Name);
|
|
||||||
poetGroups.Add(century);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AggressiveCacheEnabled)
|
|
||||||
{
|
|
||||||
_memoryCache.Set(cacheKey, poetGroups, TimeSpan.FromHours(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LastError = e.ToString();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PoetGroups = poetGroups ?? [];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
private void PrepareNextPre()
|
|
||||||
{
|
|
||||||
if (GanjoorPage == null) return;
|
|
||||||
switch (GanjoorPage.GanjoorPageType)
|
|
||||||
{
|
|
||||||
case GanjoorPageType.PoemPage:
|
|
||||||
{
|
|
||||||
if (GanjoorPage.Poem.Next != null)
|
|
||||||
{
|
|
||||||
NextUrl = GanjoorPage.PoetOrCat.Cat.FullUrl + "/" + GanjoorPage.Poem.Next.UrlSlug;
|
|
||||||
NextTitle = GanjoorPage.Poem.Next.Title + ": " + GanjoorPage.Poem.Next.Excerpt;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (GanjoorPage.Poem.MixedModeOrder > 0
|
|
||||||
&& GanjoorPage.Poem.Category.Cat.Children.Where(c => c.MixedModeOrder == 0 || c.MixedModeOrder > GanjoorPage.Poem.MixedModeOrder).Any())
|
|
||||||
{
|
|
||||||
var nextCat = GanjoorPage.Poem.Category.Cat.Children.Where(c => c.MixedModeOrder == 0 || c.MixedModeOrder > GanjoorPage.Poem.MixedModeOrder).OrderBy(c => c.MixedModeOrder).First();
|
|
||||||
NextUrl = nextCat.FullUrl;
|
|
||||||
NextTitle = nextCat.Title;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (GanjoorPage.Poem.Category.Cat.Next != null)
|
|
||||||
{
|
|
||||||
NextUrl = GanjoorPage.Poem.Category.Cat.Next.FullUrl;
|
|
||||||
NextTitle = GanjoorPage.Poem.Category.Cat.Next.Title;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GanjoorPage.Poem.Previous != null)
|
|
||||||
{
|
|
||||||
PreviousUrl = GanjoorPage.PoetOrCat.Cat.FullUrl + "/" + GanjoorPage.Poem.Previous.UrlSlug;
|
|
||||||
PreviousTitle = GanjoorPage.Poem.Previous.Title + ": " + GanjoorPage.Poem.Previous.Excerpt;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (GanjoorPage.Poem.MixedModeOrder > 0
|
|
||||||
&& GanjoorPage.Poem.Category.Cat.Children.Where(c => c.MixedModeOrder != 0 && c.MixedModeOrder < GanjoorPage.Poem.MixedModeOrder).Any())
|
|
||||||
{
|
|
||||||
var prevCat = GanjoorPage.Poem.Category.Cat.Children.Where(c => c.MixedModeOrder != 0 && c.MixedModeOrder < GanjoorPage.Poem.MixedModeOrder).OrderByDescending(c => c.MixedModeOrder).First();
|
|
||||||
PreviousUrl = prevCat.FullUrl;
|
|
||||||
PreviousTitle = prevCat.Title;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
if (GanjoorPage.Poem.Category.Cat.Previous != null)
|
|
||||||
{
|
|
||||||
PreviousUrl = GanjoorPage.Poem.Category.Cat.Previous.FullUrl;
|
|
||||||
PreviousTitle = GanjoorPage.Poem.Category.Cat.Previous.Title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case GanjoorPageType.CatPage:
|
|
||||||
{
|
|
||||||
if (GanjoorPage.PoetOrCat.Cat.Next != null)
|
|
||||||
{
|
|
||||||
NextUrl = GanjoorPage.PoetOrCat.Cat.Next.FullUrl;
|
|
||||||
NextTitle = GanjoorPage.PoetOrCat.Cat.Next.Title;
|
|
||||||
}
|
|
||||||
if (GanjoorPage.PoetOrCat.Cat.Previous != null)
|
|
||||||
{
|
|
||||||
PreviousUrl = GanjoorPage.PoetOrCat.Cat.Previous.FullUrl;
|
|
||||||
PreviousTitle = GanjoorPage.PoetOrCat.Cat.Previous.Title;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GeneratePagingBarHtml(PaginationMetadata paginationMetadata, string routeStartWithQueryStrings)
|
|
||||||
{
|
|
||||||
string htmlText = $"<div>{Environment.NewLine}";
|
|
||||||
|
|
||||||
|
|
||||||
if (paginationMetadata != null && paginationMetadata.totalPages > 1)
|
|
||||||
{
|
|
||||||
if (paginationMetadata.currentPage > 3)
|
|
||||||
{
|
|
||||||
htmlText += $"<a href=\"{routeStartWithQueryStrings.Replace("\"", "%22")}&page=1\"><div class=\"circled-number\">1</div></a>{Environment.NewLine} …";
|
|
||||||
}
|
|
||||||
for (int i = paginationMetadata.currentPage - 2; i <= (paginationMetadata.currentPage + 2); i++)
|
|
||||||
{
|
|
||||||
if (i >= 1 && i <= paginationMetadata.totalPages)
|
|
||||||
{
|
|
||||||
if (i == paginationMetadata.currentPage)
|
|
||||||
{
|
|
||||||
htmlText += $"<div class=\"circled-number-diff\">{i}</div>";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
htmlText += $"<a href=\"{routeStartWithQueryStrings.Replace("\"", "%22")}&page={i}\"><div class=\"circled-number\">{i}</div></a>{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (paginationMetadata.totalPages > (paginationMetadata.currentPage + 2))
|
|
||||||
{
|
|
||||||
htmlText += $"… <a href=\"{routeStartWithQueryStrings.Replace("\"", "%22")}&page={paginationMetadata.totalPages}\"><div class=\"circled-number\">{paginationMetadata.totalPages}</div></a>{Environment.NewLine}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlText += $"</div>{Environment.NewLine}";
|
|
||||||
return htmlText;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public string? LastError { get; set; }
|
|
||||||
|
|
||||||
public bool AggressiveCacheEnabled
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return bool.Parse(Configuration["AggressiveCacheEnabled"] ?? false.ToString());
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected readonly IConfiguration Configuration;
|
|
||||||
protected readonly HttpClient _httpClient;
|
|
||||||
private readonly IMemoryCache _memoryCache;
|
|
||||||
public IndexModel(IConfiguration configuration, HttpClient httpClient, IMemoryCache memoryCache)
|
|
||||||
{
|
|
||||||
Configuration = configuration;
|
|
||||||
_httpClient = httpClient;
|
|
||||||
_memoryCache = memoryCache;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="tg">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>@ViewData["Title"]</title>
|
|
||||||
<link rel="icon" href="~/favicon.ico" sizes="any" />
|
|
||||||
<meta name="theme-color" content="#6b3f1d" />
|
|
||||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
|
||||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
|
||||||
<link rel="stylesheet" href="~/css/tajik-ganjoor.css" asp-append-version="true" />
|
|
||||||
<link rel="stylesheet" href="~/TajikGanjoor.styles.css" asp-append-version="true" />
|
|
||||||
<script src="~/js/recitation-sync.js" asp-append-version="true"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
|
|
||||||
<div class="tg-breadcrumb">
|
|
||||||
@Html.Raw(ViewData["BreadCrumpUrls"])
|
|
||||||
</div>
|
|
||||||
<div class="flex-fill"></div>
|
|
||||||
<form class="d-flex" action="/" method="get" id="searchhorm">
|
|
||||||
<input type="search" class="form-control" placeholder="ҷустуҷӯ" aria-label="ҷустуҷӯ" name="s" id="s" value="@Model.Query">
|
|
||||||
</form>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<div class="container">
|
|
||||||
<main role="main" class="pb-3">
|
|
||||||
@RenderBody()
|
|
||||||
</main>
|
|
||||||
@if (!string.IsNullOrEmpty(ViewData["PreviousUrl"]?.ToString()) || !string.IsNullOrEmpty(ViewData["NextUrl"]?.ToString()))
|
|
||||||
{
|
|
||||||
<nav class="tg-poem-nav">
|
|
||||||
@if (!string.IsNullOrEmpty(ViewData["PreviousUrl"]?.ToString()))
|
|
||||||
{
|
|
||||||
<a class="tg-poem-nav-link tg-poem-nav-prev" href="@ViewData["PreviousUrl"]" title="@ViewData["PreviousTitle"]">
|
|
||||||
<span class="tg-poem-nav-arrow">←</span>
|
|
||||||
<span class="tg-poem-nav-text">
|
|
||||||
<span class="tg-poem-nav-caption">қаблӣ</span>
|
|
||||||
<span class="tg-poem-nav-title">@ViewData["PreviousTitle"]</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span></span>
|
|
||||||
}
|
|
||||||
@if (!string.IsNullOrEmpty(ViewData["NextUrl"]?.ToString()))
|
|
||||||
{
|
|
||||||
<a class="tg-poem-nav-link tg-poem-nav-next" href="@ViewData["NextUrl"]" title="@ViewData["NextTitle"]">
|
|
||||||
<span class="tg-poem-nav-text">
|
|
||||||
<span class="tg-poem-nav-caption">баъдӣ</span>
|
|
||||||
<span class="tg-poem-nav-title">@ViewData["NextTitle"]</span>
|
|
||||||
</span>
|
|
||||||
<span class="tg-poem-nav-arrow">→</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
</nav>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="border-top footer text-muted">
|
|
||||||
<span>Ба неруии <a href="http://persian-tajik.ir/FarsiTotajiki.aspx" target="_blank">пиршяни тоҷик</a></span>
|
|
||||||
<span><a href="@ViewData["GoogleTranslateLink"]" target="_blank">Дар ганҷури форсӣ</a></span>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
|
||||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script src="~/js/site.js" asp-append-version="true"></script>
|
|
||||||
|
|
||||||
@await RenderSectionAsync("Scripts", required: false)
|
|
||||||
|
|
||||||
|
|
||||||
@if (ViewData["TrackingScript"] != null && !string.IsNullOrEmpty(ViewData["TrackingScript"].ToString()))
|
|
||||||
{
|
|
||||||
@Html.Raw(ViewData["TrackingScript"])
|
|
||||||
}
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
|
||||||
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
@using TajikGanjoor
|
|
||||||
@namespace TajikGanjoor.Pages
|
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
@{
|
|
||||||
Layout = "_Layout";
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
using System.Text.Encodings.Web;
|
|
||||||
using System.Text.Unicode;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.Services.AddSingleton(
|
|
||||||
HtmlEncoder.Create(allowedRanges: [ UnicodeRanges.BasicLatin,
|
|
||||||
UnicodeRanges.Arabic, UnicodeRanges.Cyrillic ]));
|
|
||||||
|
|
||||||
builder.Services.AddRazorPages(options =>
|
|
||||||
{
|
|
||||||
options.Conventions.AddPageRoute("/index", "{*url}");
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
if (!app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.UseExceptionHandler("/Error");
|
|
||||||
}
|
|
||||||
app.UseStaticFiles();
|
|
||||||
|
|
||||||
app.UseRouting();
|
|
||||||
|
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
app.MapRazorPages();
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
|
||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<DeleteExistingFiles>true</DeleteExistingFiles>
|
|
||||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
|
||||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
|
||||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
|
||||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
|
||||||
<PublishProvider>FileSystem</PublishProvider>
|
|
||||||
<PublishUrl>bin\Release\net10.0-windows7.0\publish\</PublishUrl>
|
|
||||||
<WebPublishMethod>FileSystem</WebPublishMethod>
|
|
||||||
<_TargetId>Folder</_TargetId>
|
|
||||||
<SiteUrlToLaunchAfterPublish />
|
|
||||||
<TargetFramework>net10.0-windows7.0</TargetFramework>
|
|
||||||
<ProjectGuid>1c51eb06-5190-4646-84c7-5ee712c86f57</ProjectGuid>
|
|
||||||
<SelfContained>false</SelfContained>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"profiles": {
|
|
||||||
"http": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
},
|
|
||||||
"dotnetRunMessages": true,
|
|
||||||
"applicationUrl": "http://localhost:33082"
|
|
||||||
},
|
|
||||||
"IIS Express": {
|
|
||||||
"commandName": "IISExpress",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "http://localhost:37226",
|
|
||||||
"sslPort": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0-windows7.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
|
||||||
<NoWarn>1701;1702;8602;8618;8601;8604;8625;8600</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
|
||||||
<NoWarn>1701;1702;8602;8618;8601;8604;8625;8600</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\RMuseum\RMuseum.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"DetailedErrors": true,
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"TrackingScript": "<script src='https://cdn.kntr.ir/tracker.js'></script><script>kntrTracking.startTracking(\"2\", \"loggedon\");</script>",
|
|
||||||
"APIRoot": "https://api.ganjoor.net",
|
|
||||||
"GlobalAPIRoot": "https://api.ganjoor.net",
|
|
||||||
"SiteUrl": "http://localhost:33082",
|
|
||||||
"AggressiveCacheEnabled": "False",
|
|
||||||
"BannersEnabled": "False",
|
|
||||||
"ReadOnlyMode": "False",
|
|
||||||
"MaintenanceMode": "False"
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"isRoot": true,
|
|
||||||
"tools": {
|
|
||||||
"dotnet-ef": {
|
|
||||||
"version": "10.0.9",
|
|
||||||
"commands": [
|
|
||||||
"dotnet-ef"
|
|
||||||
],
|
|
||||||
"rollForward": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
html {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
html {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
|
||||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
position: relative;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin-bottom: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hilite {
|
|
||||||
color: #000;
|
|
||||||
background-color: #9cf
|
|
||||||
}
|
|
||||||
|
|
||||||
.hilite1 {
|
|
||||||
color: #000;
|
|
||||||
background-color: #fc6
|
|
||||||
}
|
|
||||||
|
|
||||||
.hilite2 {
|
|
||||||
color: #000;
|
|
||||||
background-color: #9f6
|
|
||||||
}
|
|
||||||
|
|
||||||
.hilite3 {
|
|
||||||
color: #000;
|
|
||||||
background-color: #f99
|
|
||||||
}
|
|
||||||
@ -1,859 +0,0 @@
|
|||||||
/* ==========================================================================
|
|
||||||
TajikGanjoor typography & layout
|
|
||||||
Self-hosted fonts: PT Serif (poem/body text) + PT Sans (UI chrome).
|
|
||||||
Subset used: cyrillic-ext — the plain "cyrillic" subset is MISSING the
|
|
||||||
six Tajik-specific letters (ғ ӣ қ ӯ ҳ ҷ), cyrillic-ext has them.
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'PT Serif';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/fonts/ptserif/pt-serif-cyrillic-ext-400-normal.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'PT Serif';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/fonts/ptserif/pt-serif-cyrillic-ext-400-italic.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'PT Serif';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/fonts/ptserif/pt-serif-cyrillic-ext-700-normal.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'PT Serif';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/fonts/ptserif/pt-serif-cyrillic-ext-700-italic.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'PT Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/fonts/ptsans/pt-sans-cyrillic-ext-400-normal.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'PT Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/fonts/ptsans/pt-sans-cyrillic-ext-400-italic.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'PT Sans';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/fonts/ptsans/pt-sans-cyrillic-ext-700-normal.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'PT Sans';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/fonts/ptsans/pt-sans-cyrillic-ext-700-italic.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Persian original text (interlinear comparison lines) - Cyrillic fonts above have no
|
|
||||||
Arabic-script glyphs at all, so this needs its own font. Vazirmatn is the same font
|
|
||||||
the main ganjoor.net site self-hosts for Persian, reused here for a consistent,
|
|
||||||
recognizable feel across the two sites. */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Vazirmatn';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/fonts/vazirmatn/vazirmatn-arabic-400-normal.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Vazirmatn';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url('/fonts/vazirmatn/vazirmatn-arabic-700-normal.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- base page: warm paper tone, PT Sans chrome ---------- */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--tg-paper: #faf6ee;
|
|
||||||
--tg-paper-edge: #efe6d3;
|
|
||||||
--tg-surface: #ffffff;
|
|
||||||
--tg-ink: #2b2620;
|
|
||||||
--tg-ink-soft: #5b5346;
|
|
||||||
--tg-accent: #8a5a2b;
|
|
||||||
--tg-accent-deep: #6b3f1d;
|
|
||||||
--tg-rule: #d9cdb2;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--tg-paper: #211d18;
|
|
||||||
--tg-paper-edge: #2c2620;
|
|
||||||
--tg-surface: #2c2620;
|
|
||||||
--tg-ink: #ece6d9;
|
|
||||||
--tg-ink-soft: #bfb6a4;
|
|
||||||
--tg-accent: #d1a06a;
|
|
||||||
--tg-accent-deep: #e3b988;
|
|
||||||
--tg-rule: #453d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* poet portrait circles keep a light backing regardless of theme -
|
|
||||||
the portraits are illustrations on a light background, and a
|
|
||||||
pure-white disc reads as a deliberate frame against the dark
|
|
||||||
page rather than a mismatched surface */
|
|
||||||
.tg-poet-portrait,
|
|
||||||
.tg-poet-portrait-lg,
|
|
||||||
.sitem-poet-portrait,
|
|
||||||
.tg-crumb-poet img {
|
|
||||||
background: #f4efe4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
background-color: var(--tg-paper);
|
|
||||||
color: var(--tg-ink);
|
|
||||||
font-family: 'PT Sans', Tahoma, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
background-color: var(--tg-paper) !important;
|
|
||||||
border-bottom: 1px solid var(--tg-rule) !important;
|
|
||||||
font-family: 'PT Sans', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* BreadCrumpUrls always starts with <a href="/">Ганҷур</a> (see Index.cshtml.cs) -
|
|
||||||
style that first link as the site's wordmark rather than duplicating it. */
|
|
||||||
.tg-breadcrumb {
|
|
||||||
font-family: 'PT Sans', sans-serif;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
column-gap: 4px;
|
|
||||||
row-gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-breadcrumb a:first-child {
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
text-decoration: none;
|
|
||||||
margin-inline-end: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar input[type="search"] {
|
|
||||||
font-family: 'PT Sans', sans-serif;
|
|
||||||
border: 1px solid var(--tg-rule);
|
|
||||||
background: var(--tg-surface);
|
|
||||||
color: var(--tg-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
font-family: 'PT Sans', sans-serif;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a {
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- poem prev/next navigation ---------- */
|
|
||||||
|
|
||||||
.tg-poem-nav {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: 12px;
|
|
||||||
max-width: 720px;
|
|
||||||
margin: 24px auto 8px auto;
|
|
||||||
padding-top: 14px;
|
|
||||||
border-top: 1px solid var(--tg-rule);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poem-nav-link {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
max-width: 46%;
|
|
||||||
padding: 6px 10px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--tg-ink-soft);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poem-nav-link:hover {
|
|
||||||
background-color: var(--tg-paper-edge);
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poem-nav-next {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
text-align: end;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poem-nav-arrow {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poem-nav-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poem-nav-caption {
|
|
||||||
font-family: 'PT Sans', sans-serif;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poem-nav-title {
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- poem page: printed-book feel ---------- */
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 720px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b, .b2, .n, .l {
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* one beyt (couplet): two stacked mesras, book-style, not side-by-side
|
|
||||||
columns — matches how Cyrillic verse is actually typeset (and matches
|
|
||||||
ganjoor.net's own <768px fallback), and needs no breakpoint since it's
|
|
||||||
already a single column at any width. */
|
|
||||||
.b {
|
|
||||||
max-width: 480px;
|
|
||||||
margin: 0 auto 1.35em auto;
|
|
||||||
text-align: center;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b .m1, .b .m2 {
|
|
||||||
float: none;
|
|
||||||
width: auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b .m1 p, .b .m2 p {
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1.15rem;
|
|
||||||
line-height: 1.9;
|
|
||||||
margin: 0;
|
|
||||||
text-shadow: none;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b .m2 p {
|
|
||||||
opacity: 0.92;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* rubaʼi-style already-centered couplet */
|
|
||||||
.b2 p {
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-size: 1.15rem;
|
|
||||||
line-height: 1.9;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* paragraph / single-line verses */
|
|
||||||
.n p, .l p {
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.9;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0 auto 1.2em auto;
|
|
||||||
max-width: 560px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* comments / notes: quieter, sans-serif, clearly not part of the poem */
|
|
||||||
.c {
|
|
||||||
max-width: 90%;
|
|
||||||
margin: 10px auto;
|
|
||||||
padding: 8px 14px;
|
|
||||||
background-color: var(--tg-paper-edge);
|
|
||||||
border-inline-start: 3px solid var(--tg-accent);
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c p {
|
|
||||||
font-family: 'PT Sans', sans-serif;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--tg-ink-soft);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Persian original, shown interlinear directly beneath its Tajik line (per
|
|
||||||
verse.OriginalText - see PrepareBilingualHtmlText). Deliberately smaller
|
|
||||||
and quieter than the Tajik line above it: this is a Tajik-first site, the
|
|
||||||
Persian is a reference/comparison aid, not the primary reading text.
|
|
||||||
Selectors repeat the base verse rules' specificity + the .tg-fa class so
|
|
||||||
this reliably wins regardless of source order. */
|
|
||||||
.b .m1 p.tg-fa,
|
|
||||||
.b .m2 p.tg-fa,
|
|
||||||
.b2 p.tg-fa,
|
|
||||||
.n p.tg-fa,
|
|
||||||
.l p.tg-fa,
|
|
||||||
.c p.tg-fa {
|
|
||||||
font-family: 'Vazirmatn', Tahoma, sans-serif;
|
|
||||||
font-size: 0.82em;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: var(--tg-ink-soft);
|
|
||||||
opacity: 0.85;
|
|
||||||
margin: 0.2em 0 0 0;
|
|
||||||
unicode-bidi: isolate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* poem title */
|
|
||||||
.text-center h1, .text-center h2 {
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-center h1 a, .text-center h2 a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* faint rule between poems on a listing/category page */
|
|
||||||
.item {
|
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid var(--tg-rule);
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:first-child {
|
|
||||||
border-top: none;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* search hit highlighting - keep legible on the paper background */
|
|
||||||
.hilite, .hilite1, .hilite2, .hilite3 {
|
|
||||||
padding: 0 2px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- homepage: poet card grid ---------- */
|
|
||||||
|
|
||||||
.tg-poet-grid {
|
|
||||||
max-width: 980px;
|
|
||||||
margin: 10px auto 0 auto;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
||||||
gap: 18px 12px;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--tg-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-card .tg-poet-portrait {
|
|
||||||
width: 84px;
|
|
||||||
height: 84px;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 2px solid var(--tg-rule);
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-card .tg-poet-portrait img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-card .tg-poet-name {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-size: 0.92rem;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-card:hover .tg-poet-portrait {
|
|
||||||
border-color: var(--tg-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-card:hover .tg-poet-name {
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.tg-poet-grid {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
|
|
||||||
gap: 14px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-card .tg-poet-portrait {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-card .tg-poet-name {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- poet bio page ---------- */
|
|
||||||
|
|
||||||
.tg-poet-header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-header .tg-poet-portrait-lg {
|
|
||||||
width: 128px;
|
|
||||||
height: 128px;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 3px solid var(--tg-rule);
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-header .tg-poet-portrait-lg img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* small poet portrait inlined into the breadcrumb, prefixing the poet's
|
|
||||||
name wherever it appears there (poet page's own crumb, and the first
|
|
||||||
ancestor crumb on category/poem pages, which is always the poet's root
|
|
||||||
category - see Index.cshtml.cs) */
|
|
||||||
.tg-crumb-poet {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-crumb-poet img {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
border: 1px solid var(--tg-rule);
|
|
||||||
background: #fff;
|
|
||||||
vertical-align: middle;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* GanjoorPage.HtmlText for poet/cat pages is server-generated plain
|
|
||||||
<p> tags (bio prose) followed by more <p><a>...</a></p> tags (sub-
|
|
||||||
category / poem links) with no distinguishing classes at all - the
|
|
||||||
markup is identical for both, so it's told apart here structurally:
|
|
||||||
a paragraph whose only content is a single link is a nav row, any
|
|
||||||
other paragraph is bio prose. */
|
|
||||||
.tg-poet-page p,
|
|
||||||
.tg-cat-page p {
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
line-height: 1.85;
|
|
||||||
text-align: start;
|
|
||||||
margin: 0 auto 0.9em auto;
|
|
||||||
max-width: 620px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-page p:has(> a:only-child),
|
|
||||||
.tg-cat-page p:has(> a:only-child) {
|
|
||||||
font-family: 'PT Sans', sans-serif;
|
|
||||||
font-size: 1rem;
|
|
||||||
text-align: start;
|
|
||||||
margin: 0;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border-bottom: 1px solid var(--tg-rule);
|
|
||||||
max-width: 620px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-page p:has(> a:only-child):first-of-type,
|
|
||||||
.tg-cat-page p:has(> a:only-child):first-of-type {
|
|
||||||
border-top: 1px solid var(--tg-rule);
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-page p:has(> a:only-child) a,
|
|
||||||
.tg-cat-page p:has(> a:only-child) a {
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* the excerpt trailing the link (": ..." from PrepareTajikCatHtmlTextAsync /
|
|
||||||
PrepareTajikPoetHtmlTextAsync) is plain text in the same <p>, so it can only
|
|
||||||
be dimmed via the paragraph's own color - the link's explicit color above
|
|
||||||
still wins for the title itself */
|
|
||||||
.tg-poet-page p:has(> a:only-child),
|
|
||||||
.tg-cat-page p:has(> a:only-child) {
|
|
||||||
color: var(--tg-ink-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-page p:has(> a:only-child):hover,
|
|
||||||
.tg-cat-page p:has(> a:only-child):hover {
|
|
||||||
background-color: var(--tg-paper-edge);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- search results ---------- */
|
|
||||||
|
|
||||||
.sitem {
|
|
||||||
max-width: 620px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 16px 4px;
|
|
||||||
border-top: 1px solid var(--tg-rule);
|
|
||||||
text-align: start;
|
|
||||||
display: flex;
|
|
||||||
gap: 14px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sitem:first-of-type {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sitem-poet-portrait {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
display: block;
|
|
||||||
width: 52px;
|
|
||||||
height: 52px;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 2px solid var(--tg-rule);
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sitem-poet-portrait img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sitem-poet-portrait:hover {
|
|
||||||
border-color: var(--tg-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sitem-body {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sitem h2 {
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-size: 1.15rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sitem h2 a {
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sit {
|
|
||||||
float: none;
|
|
||||||
max-width: none;
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-size: 0.98rem;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: var(--tg-ink-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sit p {
|
|
||||||
margin: 0 0 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 420px) {
|
|
||||||
.sitem-poet-portrait {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- pagination bar (GeneratePagingBarHtml in Index.cshtml.cs) ---------- */
|
|
||||||
|
|
||||||
.circled-number, .circled-number-diff {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
margin: 2px;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-family: 'PT Sans', sans-serif;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circled-number {
|
|
||||||
border: 1px solid var(--tg-rule);
|
|
||||||
color: var(--tg-ink-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.circled-number:hover {
|
|
||||||
border-color: var(--tg-accent);
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
.circled-number-diff {
|
|
||||||
background-color: var(--tg-accent-deep);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- homepage: century grouping + inline poet search ---------- */
|
|
||||||
|
|
||||||
.tg-century-heading {
|
|
||||||
max-width: 980px;
|
|
||||||
margin: 28px auto 4px auto;
|
|
||||||
padding: 0 8px;
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
text-align: start;
|
|
||||||
border-bottom: 1px solid var(--tg-rule);
|
|
||||||
padding-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-century-nav {
|
|
||||||
max-width: 980px;
|
|
||||||
margin: 8px auto 0 auto;
|
|
||||||
padding: 0 8px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-search {
|
|
||||||
max-width: 480px;
|
|
||||||
margin: 20px auto 0 auto;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-search input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-family: 'PT Sans', sans-serif;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
border: 1px solid var(--tg-rule);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--tg-surface);
|
|
||||||
color: var(--tg-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-search input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--tg-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* search results grid only takes up space once populated by tgInlineSearch (site.js) */
|
|
||||||
.tg-poet-search .tg-poet-grid:empty {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-poet-search .tg-poet-grid {
|
|
||||||
margin-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* century-grouped vs. alphabetical homepage view switch */
|
|
||||||
.tg-view-switch {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
max-width: 480px;
|
|
||||||
margin: 18px auto 0 auto;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-view-switch-btn {
|
|
||||||
font-family: 'PT Sans', sans-serif;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding: 7px 16px;
|
|
||||||
border: 1px solid var(--tg-rule);
|
|
||||||
border-radius: 20px;
|
|
||||||
background: var(--tg-surface);
|
|
||||||
color: var(--tg-ink-soft);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-view-switch-btn:hover {
|
|
||||||
border-color: var(--tg-accent);
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-view-switch-btn.active {
|
|
||||||
background: var(--tg-accent-deep);
|
|
||||||
border-color: var(--tg-accent-deep);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- error page ---------- */
|
|
||||||
|
|
||||||
.text-danger {
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
color: var(--tg-accent-deep) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- poem recitations (audio) ---------- */
|
|
||||||
|
|
||||||
.tg-recitations {
|
|
||||||
max-width: 560px;
|
|
||||||
margin: 28px auto 0 auto;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid var(--tg-rule);
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-recitations-title {
|
|
||||||
font-family: 'PT Serif', serif;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-recitation {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
background-color: var(--tg-paper-edge);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-recitation audio {
|
|
||||||
max-width: 100%;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-recitation-caption {
|
|
||||||
font-family: 'PT Sans', sans-serif;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
color: var(--tg-ink-soft);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-recitation-title {
|
|
||||||
color: var(--tg-ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-recitation-download {
|
|
||||||
color: var(--tg-accent-deep);
|
|
||||||
text-decoration: none;
|
|
||||||
margin-inline-start: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-recitation-download:hover {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.tg-recitation audio {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* verse currently being recited, highlighted by recitation-sync.js */
|
|
||||||
.tg-verse-active {
|
|
||||||
color: var(--tg-accent-deep) !important;
|
|
||||||
background-color: var(--tg-paper-edge);
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- sticky mini audio-player (recitation seek controls stay
|
|
||||||
reachable while scrolling the poem text) ---------- */
|
|
||||||
|
|
||||||
.tg-sticky-player {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
background: var(--tg-paper-edge);
|
|
||||||
border-top: 1px solid var(--tg-rule);
|
|
||||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-sticky-player-narrator {
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--tg-ink);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-sticky-player-audio-slot {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-sticky-player-audio-slot audio {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-sticky-player-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--tg-ink);
|
|
||||||
padding: 4px 6px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tg-sticky-player-btn:hover {
|
|
||||||
color: var(--tg-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.tg-sticky-player-narrator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 240 B |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 44 KiB |