Projects (GanjooRazor -> DiwanRazor, GanjoorService.sln -> DiwanService.sln), files, folders, classes, namespaces, settings, API routes (/api/diwan) and DB tables (Diwan*). External addresses (ganjoor.net, github.com/ganjoor) unchanged. Migrations renamed consistently (no pending model changes); requires a fresh database. Upstream RUNNING_LOCALLY/SEMANTIC_SEARCH docs archived unmodified under docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
450 lines
20 KiB
C#
450 lines
20 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using RMuseum.DbContext;
|
||
using RMuseum.Models.Diwan;
|
||
using RMuseum.Models.Diwan.ViewModels;
|
||
using RSecurityBackend.Models.Generic;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Data;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using RSecurityBackend.Services.Implementation;
|
||
using DNTPersianUtils.Core;
|
||
using System.IO;
|
||
using RSecurityBackend.Models.Image;
|
||
using FluentFTP;
|
||
|
||
namespace RMuseum.Services.Implementation
|
||
{
|
||
/// <summary>
|
||
/// IDiwanService implementation
|
||
/// </summary>
|
||
public partial class DiwanService : IDiwanService
|
||
{
|
||
|
||
/// <summary>
|
||
/// set category poem format tag for poems consisting of a single whole poem section
|
||
/// </summary>
|
||
/// <param name="catId"></param>
|
||
/// <param name="format"></param>
|
||
/// <returns></returns>
|
||
public async Task<RServiceResult<bool>> SetCategoryPoemFormatAsync(int catId, DiwanPoemFormat? format)
|
||
{
|
||
try
|
||
{
|
||
var poemList = await _context.DiwanPoems.Where(p => p.CatId == catId).ToListAsync();
|
||
foreach (var poem in poemList)
|
||
{
|
||
var poemSections = await _context.DiwanPoemSections.Where(s => s.PoemId == poem.Id && s.SectionType == PoemSectionType.WholePoem).ToListAsync();
|
||
if(poemSections.Count == 1)
|
||
{
|
||
foreach (var section in poemSections)
|
||
{
|
||
section.PoemFormat = format;
|
||
}
|
||
_context.UpdateRange(poemSections);
|
||
}
|
||
}
|
||
_context.UpdateRange(poemList);
|
||
await _context.SaveChangesAsync();
|
||
return new RServiceResult<bool>(true);
|
||
}
|
||
catch (Exception exp)
|
||
{
|
||
return new RServiceResult<bool>(false, exp.ToString());
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// set category poems language tag
|
||
/// </summary>
|
||
/// <param name="catId"></param>
|
||
/// <param name="language"></param>
|
||
/// <returns></returns>
|
||
public async Task<RServiceResult<bool>> SetCategoryLanguageTagAsync(int catId, string language)
|
||
{
|
||
try
|
||
{
|
||
var poemList = await _context.DiwanPoems.Where(p => p.CatId == catId).ToListAsync();
|
||
var lan = await _context.DiwanLanguages.AsNoTracking().Where(l => l.Code == language).SingleAsync();
|
||
foreach (var poem in poemList)
|
||
{
|
||
poem.Language = language;
|
||
var poemSections = await _context.DiwanPoemSections.Where(s => s.PoemId == poem.Id && s.SectionType == PoemSectionType.WholePoem).ToListAsync();
|
||
foreach (var section in poemSections)
|
||
{
|
||
section.Language = language;
|
||
}
|
||
var verses = await _context.DiwanVerses.Where(v => v.PoemId == poem.Id && v.VersePosition != VersePosition.Comment).ToListAsync();
|
||
foreach (var verse in verses)
|
||
{
|
||
verse.LanguageId = lan.Id;
|
||
}
|
||
_context.UpdateRange(verses);
|
||
_context.UpdateRange(poemSections);
|
||
}
|
||
_context.UpdateRange(poemList);
|
||
await _context.SaveChangesAsync();
|
||
return new RServiceResult<bool>(true);
|
||
}
|
||
catch (Exception exp)
|
||
{
|
||
return new RServiceResult<bool>(false, exp.ToString());
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// re sulg cat poems
|
||
/// </summary>
|
||
/// <param name="catId"></param>
|
||
/// <returns></returns>
|
||
public async Task<RServiceResult<bool>> BatchReSlugCatPoems(int catId)
|
||
{
|
||
try
|
||
{
|
||
var poems = await _context.DiwanPoems.Where(p => p.CatId == catId).OrderBy(p => p.Id).ToListAsync();
|
||
if (poems.Count == 0)
|
||
return new RServiceResult<bool>(true);
|
||
|
||
var catPage = await _context.DiwanPages.Where(p => p.DiwanPageType == DiwanPageType.CatPage && p.CatId == catId).SingleOrDefaultAsync();
|
||
if (catPage == null)
|
||
{
|
||
var catItSelf = await _context.DiwanCategories.AsNoTracking().Where(c => c.Id == catId).SingleAsync();
|
||
catPage = await _context.DiwanPages.Where(p => p.DiwanPageType == DiwanPageType.PoetPage && p.PoetId == catItSelf.PoetId).SingleAsync();
|
||
}
|
||
|
||
for (int i = 0; i < poems.Count; i++)
|
||
{
|
||
if (poems[i].UrlSlug.IndexOf("sh") != 0)
|
||
{
|
||
return new RServiceResult<bool>(false, $"{poems[i].FullUrl}");
|
||
}
|
||
poems[i].UrlSlug = $"sh{i + 1}";
|
||
poems[i].FullUrl = $"{catPage.FullUrl}/{poems[i].UrlSlug}";
|
||
}
|
||
|
||
foreach (var poem in poems)
|
||
{
|
||
var page = await _context.DiwanPages.Where(p => p.Id == poem.Id).SingleAsync();
|
||
page.UrlSlug = poem.UrlSlug;
|
||
page.FullUrl = poem.FullUrl;
|
||
_context.DiwanPages.Update(page);
|
||
}
|
||
|
||
_context.DiwanPoems.UpdateRange(poems);
|
||
|
||
await _context.SaveChangesAsync();
|
||
|
||
return new RServiceResult<bool>(true);
|
||
}
|
||
catch (Exception exp)
|
||
{
|
||
return new RServiceResult<bool>(false, exp.ToString());
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// batch rename
|
||
/// </summary>
|
||
/// <param name="catId"></param>
|
||
/// <param name="model"></param>
|
||
/// <param name="userId"></param>
|
||
/// <returns></returns>
|
||
public async Task<RServiceResult<string[]>> BatchRenameCatPoemTitles(int catId, DiwanBatchNamingModel model, Guid userId)
|
||
{
|
||
var poems = await _context.DiwanPoems.Where(p => p.CatId == catId).OrderBy(p => p.Id).ToListAsync();
|
||
if (poems.Count == 0)
|
||
return new RServiceResult<string[]>(new string[] { });
|
||
|
||
var catPage = await _context.DiwanPages.Where(p => p.DiwanPageType == DiwanPageType.CatPage && p.CatId == catId).SingleOrDefaultAsync();
|
||
if (catPage == null)
|
||
{
|
||
var catItSelf = await _context.DiwanCategories.AsNoTracking().Where(c => c.Id == catId).SingleAsync();
|
||
catPage = await _context.DiwanPages.Where(p => p.DiwanPageType == DiwanPageType.PoetPage && p.PoetId == catItSelf.PoetId).SingleAsync();
|
||
}
|
||
|
||
if (model.RemovePreviousPattern)
|
||
{
|
||
char[] numbers = "0123456789۰۱۲۳۴۵۶۷۸۹".ToArray();
|
||
foreach (var poem in poems)
|
||
{
|
||
_context.DiwanPageSnapshots.Add
|
||
(
|
||
new DiwanPageSnapshot()
|
||
{
|
||
DiwanPageId = poem.Id,
|
||
MadeObsoleteByUserId = userId,
|
||
Title = poem.Title,
|
||
Note = "تغییر نام گروهی اشعار بخش",
|
||
RecordDate = DateTime.Now
|
||
}
|
||
);
|
||
int index = poem.Title.IndexOfAny(numbers);
|
||
if (index != 0)
|
||
{
|
||
while ((index + 1) < poem.Title.Length)
|
||
{
|
||
if (numbers.Contains(poem.Title[index + 1]))
|
||
index++;
|
||
else
|
||
break;
|
||
}
|
||
poem.Title = poem.Title[(index + 1)..].Trim();
|
||
if (poem.Title.IndexOf('-') == 0)
|
||
{
|
||
poem.Title = poem.Title[1..].Trim();
|
||
}
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(model.RemoveSetOfCharacters))
|
||
{
|
||
foreach (var c in model.RemoveSetOfCharacters)
|
||
{
|
||
poem.Title = poem.Title.Replace($"{c}", "");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
for (int i = 0; i < poems.Count; i++)
|
||
{
|
||
if (poems[i].Title.Length > 0)
|
||
{
|
||
poems[i].Title = $"{model.StartWithNotIncludingSpaces} {(i + 1).ToPersianNumbers()} - {poems[i].Title}";
|
||
}
|
||
else
|
||
{
|
||
poems[i].Title = $"{model.StartWithNotIncludingSpaces} {(i + 1).ToPersianNumbers()}";
|
||
}
|
||
|
||
poems[i].FullTitle = $"{catPage.FullTitle} » {poems[i].Title}";
|
||
|
||
}
|
||
|
||
if (!model.Simulate)
|
||
{
|
||
foreach (var poem in poems)
|
||
{
|
||
var page = await _context.DiwanPages.Where(p => p.Id == poem.Id).SingleAsync();
|
||
page.Title = poem.Title;
|
||
page.FullTitle = poem.FullTitle;
|
||
_context.DiwanPages.Update(page);
|
||
}
|
||
|
||
_context.DiwanPoems.UpdateRange(poems);
|
||
|
||
await _context.SaveChangesAsync();
|
||
}
|
||
|
||
return new RServiceResult<string[]>(poems.Select(p => p.Title).ToArray());
|
||
}
|
||
|
||
/// <summary>
|
||
/// list of category saved duplicated poems
|
||
/// </summary>
|
||
/// <param name="catId"></param>
|
||
/// <returns></returns>
|
||
public async Task<RServiceResult<DiwanDuplicateViewModel[]>> GetCategoryDuplicates(int catId)
|
||
{
|
||
try
|
||
{
|
||
List<DiwanDuplicateViewModel> dups = new List<DiwanDuplicateViewModel>();
|
||
|
||
var poems = await _context.DiwanPoems.AsNoTracking().Where(p => p.CatId == catId).OrderBy(p => p.Id).ToListAsync();
|
||
foreach (var poem in poems)
|
||
{
|
||
var firstVerse = await _context.DiwanVerses.AsNoTracking().Where(v => v.PoemId == poem.Id).OrderBy(v => v.VOrder).FirstAsync();
|
||
var dupPoem = await _context.DiwanDuplicates.AsNoTracking().Where(d => d.SrcCatId == catId && d.SrcPoemId == poem.Id).FirstOrDefaultAsync();
|
||
DiwanPoem destPoem = dupPoem == null || dupPoem.DestPoemId == null ? null :
|
||
await _context.DiwanPoems.AsNoTracking().Where(p => p.Id == dupPoem.DestPoemId).SingleAsync();
|
||
var destPoemFirstVerse = dupPoem == null || dupPoem.DestPoemId == null ? null :
|
||
await _context.DiwanVerses.AsNoTracking().Where(v => v.PoemId == dupPoem.DestPoemId).OrderBy(v => v.VOrder).FirstAsync();
|
||
dups.Add
|
||
(
|
||
new DiwanDuplicateViewModel()
|
||
{
|
||
Id = dupPoem == null ? 0 : dupPoem.Id,
|
||
SrcPoemId = poem.Id,
|
||
SrcPoemFullTitle = poem.FullTitle,
|
||
SrcPoemFullUrl = poem.FullUrl,
|
||
FirstVerse = firstVerse.Text,
|
||
DestPoemId = dupPoem == null ? null : dupPoem.DestPoemId,
|
||
DestPoemFullTitle = dupPoem == null ? "" : destPoem.FullTitle,
|
||
DestPoemFullUrl = dupPoem == null ? "" : destPoem.FullUrl,
|
||
DestPoemFirstVerse = destPoemFirstVerse == null ? null : destPoemFirstVerse.Text,
|
||
}
|
||
);
|
||
}
|
||
|
||
return new RServiceResult<DiwanDuplicateViewModel[]>(dups.ToArray());
|
||
}
|
||
catch (Exception exp)
|
||
{
|
||
return new RServiceResult<DiwanDuplicateViewModel[]>(null, exp.ToString());
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Update category extra info
|
||
/// </summary>
|
||
/// <param name="catId"></param>
|
||
/// <param name="bookName"></param>
|
||
/// <param name="imageId"></param>
|
||
/// <param name="sumUpSubsGeoLocations"></param>
|
||
/// <param name="mapName"></param>
|
||
/// <returns></returns>
|
||
public async Task<RServiceResult<DiwanCatViewModel>> SetCategoryExtraInfo(int catId, string bookName, Guid? imageId, bool sumUpSubsGeoLocations, string mapName)
|
||
{
|
||
try
|
||
{
|
||
var cat = await _context.DiwanCategories.Where(c => c.Id == catId).SingleOrDefaultAsync();
|
||
if (cat == null)
|
||
return new RServiceResult<DiwanCatViewModel>(null);//not found
|
||
if (!string.IsNullOrEmpty(bookName))
|
||
{
|
||
if (cat.CatType != DiwanCatType.Book)
|
||
{
|
||
return new RServiceResult<DiwanCatViewModel>(null, "cat.CatType != DiwanCatType.Book");
|
||
}
|
||
}
|
||
if (imageId != null)
|
||
{
|
||
RServiceResult<RImage> img =
|
||
await _imageFileService.GetImage((Guid)imageId);
|
||
if (!string.IsNullOrEmpty(img.ExceptionString))
|
||
{
|
||
return new RServiceResult<DiwanCatViewModel>(null, img.ExceptionString);
|
||
}
|
||
|
||
if (bool.Parse(Configuration.GetSection("ExternalFTPServer")["UploadEnabled"]))
|
||
{
|
||
var ftpClient = new AsyncFtpClient
|
||
(
|
||
Configuration.GetSection("ExternalFTPServer")["Host"],
|
||
Configuration.GetSection("ExternalFTPServer")["Username"],
|
||
Configuration.GetSection("ExternalFTPServer")["Password"]
|
||
);
|
||
ftpClient.ValidateCertificate += FtpClient_ValidateCertificate;
|
||
await ftpClient.AutoConnect();
|
||
ftpClient.Config.RetryAttempts = 3;
|
||
RServiceResult<string> imgPath = _imageFileService.GetImagePath(img.Result);
|
||
if (!string.IsNullOrEmpty(imgPath.ExceptionString))
|
||
return new RServiceResult<DiwanCatViewModel>(null, imgPath.ExceptionString);
|
||
|
||
var localFilePath = imgPath.Result;
|
||
var remoteFilePath = $"{Configuration.GetSection("ExternalFTPServer")["RootPath"]}/images/CategoryImages/{Path.GetFileName(localFilePath)}";
|
||
await ftpClient.UploadFile(localFilePath, remoteFilePath);
|
||
await ftpClient.Disconnect();
|
||
}
|
||
|
||
if (bool.Parse(Configuration.GetSection("BackupFTPServer")["UploadEnabled"]))
|
||
{
|
||
var ftpClient = new AsyncFtpClient
|
||
(
|
||
Configuration.GetSection("BackupFTPServer")["Host"],
|
||
Configuration.GetSection("BackupFTPServer")["Username"],
|
||
Configuration.GetSection("BackupFTPServer")["Password"]
|
||
);
|
||
ftpClient.ValidateCertificate += FtpClient_ValidateCertificate;
|
||
await ftpClient.AutoConnect();
|
||
ftpClient.Config.RetryAttempts = 3;
|
||
RServiceResult<string> imgPath = _imageFileService.GetImagePath(img.Result);
|
||
if (!string.IsNullOrEmpty(imgPath.ExceptionString))
|
||
return new RServiceResult<DiwanCatViewModel>(null, imgPath.ExceptionString);
|
||
|
||
var localFilePath = imgPath.Result;
|
||
var remoteFilePath = $"{Configuration.GetSection("BackupFTPServer")["RootPath"]}/images/CategoryImages/{Path.GetFileName(localFilePath)}";
|
||
await ftpClient.UploadFile(localFilePath, remoteFilePath);
|
||
await ftpClient.Disconnect();
|
||
}
|
||
}
|
||
cat.BookName = bookName;
|
||
cat.RImageId = imageId;
|
||
cat.SumUpSubsGeoLocations = sumUpSubsGeoLocations;
|
||
cat.MapName = mapName;
|
||
_context.Update(cat);
|
||
await _context.SaveChangesAsync();
|
||
return new RServiceResult<DiwanCatViewModel>
|
||
(
|
||
new DiwanCatViewModel()
|
||
{
|
||
RImageId = imageId,
|
||
}
|
||
);
|
||
|
||
}
|
||
catch (Exception exp)
|
||
{
|
||
return new RServiceResult<DiwanCatViewModel>(null, exp.ToString());
|
||
}
|
||
}
|
||
|
||
private async Task _FindCategoryPoemsRhymesInternal(int catId, bool retag)
|
||
{
|
||
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($"FindCategoryPoemsRhymes Cat {catId}", "Query data")).Result;
|
||
try
|
||
{
|
||
var poems = await context.DiwanPoems.AsNoTracking().Where(p => p.CatId == catId).ToListAsync();
|
||
|
||
int i = 0;
|
||
foreach (var poem in poems)
|
||
{
|
||
var verses = await context.DiwanVerses.AsNoTracking().Where(v => v.PoemId == poem.Id).OrderBy(v => v.VOrder).ToListAsync();
|
||
var sections = await context.DiwanPoemSections.Where(s => s.PoemId == poem.Id).ToListAsync();
|
||
foreach (var section in sections)
|
||
{
|
||
if (retag || string.IsNullOrEmpty(section.RhymeLetters))
|
||
{
|
||
await jobProgressServiceEF.UpdateJob(job.Id, i++);
|
||
var sectionVerses = FilterSectionVerses(section, verses);
|
||
try
|
||
{
|
||
var res = LanguageUtils.FindRhyme(sectionVerses);
|
||
if (!string.IsNullOrEmpty(res.Rhyme))
|
||
{
|
||
section.RhymeLetters = res.Rhyme;
|
||
context.DiwanPoemSections.Update(section);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
}
|
||
}
|
||
}
|
||
await jobProgressServiceEF.UpdateJob(job.Id, 99);
|
||
await context.SaveChangesAsync();
|
||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", true);
|
||
}
|
||
catch (Exception exp)
|
||
{
|
||
await jobProgressServiceEF.UpdateJob(job.Id, 100, "", false, exp.ToString());
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// find category poem rhymes
|
||
/// </summary>
|
||
/// <param name="catId"></param>
|
||
/// <param name="retag"></param>
|
||
/// <returns></returns>
|
||
public RServiceResult<bool> FindCategoryPoemsRhymes(int catId, bool retag)
|
||
{
|
||
_backgroundTaskQueue.QueueBackgroundWorkItem
|
||
(
|
||
async token =>
|
||
{
|
||
await _FindCategoryPoemsRhymesInternal(catId, retag);
|
||
});
|
||
|
||
return new RServiceResult<bool>(true);
|
||
}
|
||
}
|
||
}
|