Files
A69WD/Src/A69WD/Service/Service_Import_Milu.cs
T

399 lines
21 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using A69WD.Data;
using A69WD.Net;
using HtmlAgilityPack;
namespace A69WD.Service {
public class Service_Import_Milu {
// Thread worker che consuma la coda
private Thread _workerThread;
private readonly StatusStrip _sb;
private string _path;
// Contatori
private int _totali = 0;
private int _completati = 0;
private int _duplicati = 0;
private int _errori = 0;
public Service_Import_Milu(StatusStrip sb) {
_sb = sb;
}
public void StartWorker(string path) {
_path = path;
_workerThread = new Thread(ProcessWorking) { IsBackground = true };
_workerThread.Start();
}
public void StopWorker() {
;
}
private async void ProcessWorking() {
// Seleziono 1000 file in modo casuale dalla directory indicata:
// - Ottieni tutti i file nel percorso
string[] files = System.IO.Directory.GetFiles(Cfg.Settings.PathImport, "*chapter=all");
int MaxNFile = files.Length < Cfg.Settings.MaxNumFile ? files.Length : Cfg.Settings.MaxNumFile;
// - Mescola l'elenco dei file in modo casuale e seleziona i primi MaxNFile
// Usiamo LINQ per mescolare (Shuffle) l'array e prendere i primi MaxNFile
// Si noti che mescolare casualmente l'intero array può essere costoso per molte migliaia di file.
// Alternativamente, si può usare un algoritmo di campionamento casuale (come Fisher-Yates) se l'efficienza è cruciale,
// ma per 10 elementi da un elenco ragionevole, questo è semplice ed efficace.
Random rng = new Random();
var fileCasuali = files
.OrderBy(f => rng.Next()) // Ordina in base a un numero casuale
.Take(MaxNFile) // Prendi i primi MaxNFile
.ToList(); // Converti in List<string> (opzionale, ma utile)
// aggiorno contatori
_totali = fileCasuali.Count;
_completati = 0;
_duplicati = 0;
_errori = 0;
UpdateCounters();
// Verifico cartelle di destinazione
string pathExport = System.IO.Path.Combine(Cfg.Settings.PathApplicationRoot, "Export");
if (!Directory.Exists(pathExport)) Directory.CreateDirectory(pathExport);
if (!Directory.Exists(Cfg.Settings.PathSrcDone)) Directory.CreateDirectory(Cfg.Settings.PathSrcDone);
if (!Directory.Exists(Cfg.Settings.PathSrcErrOnExtract)) Directory.CreateDirectory(Cfg.Settings.PathSrcErrOnExtract);
if (!Directory.Exists(Cfg.Settings.PathSrcErrOnSave)) Directory.CreateDirectory(Cfg.Settings.PathSrcErrOnSave);
// Ciclo su ciascun file
foreach (string file in fileCasuali) {
string name = System.IO.Path.GetFileName(file);
Logger.Warning($"Import - Start estrazione racconto: {name}");
List<cRac> racs = await EstraiRacs(file);
if (racs != null) {
bool saveErr = false;
foreach (cRac rac in racs) {
if (rac.Validate()) {
// racconto valido:
// // - lo esporto come xml
// string nam = $"Milu_Rac_{rac.strIDrac}.xml";
// rac.SaveToXmlFile(System.IO.Path.Combine(pathExport, nam));
// - lo salvo sul DB
int res = App.DB.ImportRac(rac); // res=id del racconto nel DB
if (res < 1) {
// errore nel salvataggio
saveErr = true;
break;
} else {
// salvataggio ok
//saveErr = false;
}
} else {
Logger.Warning($"Import - estrazione errata: {name}");
saveErr = true;
break;
}
}
if (saveErr) {
// errore nel salvataggio dei racconti estratti dal file
Logger.Warning($"Import - errore nel salvataggio: {name}");
Interlocked.Increment(ref _errori);
// - Sposto il file
System.IO.File.Move(file, System.IO.Path.Combine(Cfg.Settings.PathSrcErrOnSave, System.IO.Path.GetFileName(file)));
} else {
// salvataggio ok
Logger.Warning($"Import - racconti salvati: {name}");
Interlocked.Increment(ref _completati);
// - Sposto il file
System.IO.File.Move(file, System.IO.Path.Combine(Cfg.Settings.PathSrcDone, System.IO.Path.GetFileName(file)));
}
} else {
Logger.Warning($"Import - estrazione errata: {name}");
Interlocked.Increment(ref _errori);
// - Sposto il file
System.IO.File.Move(file, System.IO.Path.Combine(Cfg.Settings.PathSrcErrOnExtract, System.IO.Path.GetFileName(file)));
}
UpdateCounters();
}
UpdateCounters();
}
private void UpdateCounters() {
if (_sb.InvokeRequired) {
_sb.Invoke(new Action(UpdateCounters));
return;
}
int daFare = _totali - _completati - _errori;
_sb.Items["sbStatusLabel"].Text = $"{_totali}|{_completati}|{_errori}";
_sb.Items["sbStatusLabel"].ToolTipText = "Totali | Completati | Error";
(_sb.Items["sbProgressBar"] as ToolStripProgressBar).Maximum = _totali > 0 ? _totali : 1;
(_sb.Items["sbProgressBar"] as ToolStripProgressBar).Value = _completati + _errori;
}
public async Task<List<cRac>> EstraiRacs(string FileFullPath) {
/*
* Analisi file dei racconti Milu con il vecchio formato:
*
* - I file con un racconto valido hanno la seguente struttura:
*
* {...}
* <div id="pagetitle">
* <a href="viewstory.php?sid={IDrac}">{Titolo}</a>
* by
* <a href="viewuser.php?uid={IDaut}">{Autor}</a>
* </div>
* <div class="infobox">
* <div class="content">
* <span class="label">Summary: </span>{Riassunto}<br />
* <span class="label">Categories:</span>
* <a href='browse.php?type=categories&amp;catid={IDcat}'>{categoria}</a>
* <span class="label">Characters: </span> {Characters}<br />
* <span class='label'>Genres: </span> {Genres}<br />
* <span class='label'>Warnings: </span> {Warnings}<br />
* <span class="label">Challenges:</span> {Challenges}<br />
* <span class="label">Series:</span> {Series}<br />
* <span class="label">Chapters: </span> {Chapters}
* <span class="label">Completed:</span> {Completed}
* <span class="label">Word count:</span> {WordCount}
* <span class="label">Read:</span> {Read}
* <span class="label"> Published: </span>{Published}
* <span class="label">Updated:</span> }Updated}
* </div>
* </div>
* <p><b>1.</b> <a href='#1'>{Tit1}</a> by {Autor}</p>
* <p><b>2.</b> <a href='#2'>{Tit2}</a> by {Autor}</p>
* <p><b>3.</b> <a href='#3'>{Tit3}</a> by {Autor}</p>
* <div class='chaptertitle'><a name='1'></a>{Tit1} by {Autor}</div>
* <div class='chapter'>{Trama1}</div>
* <div class='toplink'><a href='#1'>Torna all'indice</a></div>
* <div class='chaptertitle'><a name='2'></a>{Tit2} by {Autor}</div>
* <div class='chapter'>{Trama2}</div>
* <div class='toplink'><a href='#1'>Torna all'indice</a></div>
* <div class='chaptertitle'><a name='3'></a>{Tit3} by {Autor}</div>
* <div class='chapter'>{Trama3}</div>
* <div class='toplink'><a href='#1'>Torna all'indice</a></div>
* {...}
* <div id="archivedat">Questo racconto è attualmente archiviato su <a href="{URLrac}">{URLrac}</a></div>
*
* Quindi per ricavare i dati del racconto:
* - Ricavo tag pagetitle
* - Se pagetitle vuoto o size<=2kb racconto nullo, inserisco log e proseguo
* - Se pagetitle non nullo allora ho un racconto valido e proseguo l'estrazione.
* - Da pagetitle ricavo il titolo del racconto.
* - dal testo del tag infobox.content ricavo filtrando i vari parametri del racconto
* - Dall'elenco dei tag chaptertitle ricavo i sottotitoli dei singoli racconti ATDB
* - Dall'elenco dei tag chapter ricavo le trame dei singoli racconti.
* - Occorre verificare per bene che l'ordine venga rispettato in modo da ordinarli correttamente.
* - Infine dal tag archivedat.a ricavo l'URL del racconto per verifica che sia come il link.
* - Ricavati tutti i dati inserisco sul DB tutti i singoli capitoli come singoli racconti ATDB assegnando le parti comuni a tutti i racconti.
*
*
*/
cRac rac = new cRac();
List<cRac> racs = new List<cRac>();
HtmlNodeCollection nodes;
try {
Console.WriteLine($"Old Milu - Importo: {FileFullPath}");
string filePathName = System.IO.Path.GetFileName(FileFullPath);
// Ricavo il nodo radice del racconto:
HtmlAgilityPack.HtmlDocument tdoc = new HtmlAgilityPack.HtmlDocument();
tdoc.Load(FileFullPath);
HtmlNode root = tdoc.DocumentNode;
if (root == null) {
// racconto non presente
return null;
}
List<string> origini = null;
List<string> tags = null;
List<cNota> notes = null;
string titolo = string.Empty;
string autore = string.Empty;
DateTime creato = DateTime.Now;
string infoCategories = string.Empty;
string infoPublished = string.Empty;
string infoCharacters = string.Empty;
string infoGenres = string.Empty;
string infoWarnings = string.Empty;
string infoChallenges = string.Empty;
string infoSeries = string.Empty;
string infoChapters = string.Empty;
string infoCompleted = string.Empty;
string infoWordCount = string.Empty;
string infoRead = string.Empty;
#region Info comuni a tutti i capitoli del racconto:
#region Origine
// Origine fissa:
origini = new List<string> { "Old Milu" };
#endregion
#region Titolo & Autore
// Titolo e autore si ricavano assieme: <div id="pagetitle"><a href="viewstory.php?sid={IDrac}">{Titolo}</a> by <a href="viewuser.php?uid={IDaut}">{Autor}</a></div>
try {
nodes = root.SelectNodes("//div[@id='pagetitle']//a[@href]");
foreach (HtmlNode nd in nodes) {
string href = nd.Attributes["href"].Value;
if (href.StartsWith("viewstory.php?sid=")) {
titolo = cTools.FormattaTesto(nd.InnerText.Trim(), false);
} else {
if (href.StartsWith("viewuser.php?uid=")) {
autore = cTools.FormattaTesto(nd.InnerText.Trim(), false);
}
}
}
if (string.IsNullOrEmpty(titolo)) {
Logger.Error($"Old Milu - Import - No Title - {filePathName}");
return null;
} else {
if (string.IsNullOrEmpty(autore)) {
Logger.Error($"Old Milu - Import - No Autor - {filePathName}");
return null;
}
}
} catch (Exception ex) { Logger.Error(string.Format("Fallita estrazione titolo/autore dal file {0}\r\n{1}", filePathName, ex.Message)); return null; }
#endregion
#region Tags: --> Testo dei nodi <div class="infobox">< div class="content"><a href='browse.php?type=categories&amp;catid={IDcat}'>{categoria}</a>
try {
nodes = root.SelectNodes("//div[@class='infobox']//div[@class='content']//a");
List<string> items = new List<string>();
foreach (HtmlNode nd in nodes) {
string href = nd.Attributes["href"].Value;
if (href.StartsWith("browse.php?type=categories")) {
items.Add(cTools.FormattaTesto(nd.InnerText.Replace("genere", "").Trim(), false));
}
}
//items = items
// .OrderBy(s => s) // ordina alfabeticamente
// .Distinct() // elimina duplicati
// .ToList();
cTools.Insert_Format_Tags(ref tags, items);
if (tags.Count<1) {
Logger.Error($"Old Milu - Import - No Tags - {filePathName}");
return null;
}
} catch (Exception ex) { Logger.Warning(string.Format("Fallita estrazione Tags dal file {0}\r\n{1}", filePathName, ex.Message)); return null; }
#endregion
#region Creato & Notes
// Estraggo le informazioni generali
// Data estreatta dal testo del nodo <div class="infobox">< div class="content"> fra "Published{Published}Updated:
try {
HtmlNode nd = root.SelectSingleNode("//div[@class='infobox']//div[@class='content']");
string txt = nd.InnerText.Trim().Replace("&nbsp;", "");
infoPublished = TxtExtract(txt, "Published: ", "Updated: ");
infoCategories = TxtExtract(txt, "Categories:", "Characters:");
infoCharacters = TxtExtract(txt, "Characters:", "Genres:");
infoGenres = TxtExtract(txt, "Genres:", "Warnings:");
infoWarnings = TxtExtract(txt, "Warnings:", "Challenges:");
infoChallenges = TxtExtract(txt, "Challenges:", "Series:");
infoSeries = TxtExtract(txt, "Series:", "Chapters");
infoChapters = TxtExtract(txt, "Chapters:", "Completed");
infoCompleted = TxtExtract(txt, "Completed:", "Word count:");
infoWordCount = TxtExtract(txt, "Word count:", "Read:");
infoRead = TxtExtract(txt, "Read:", "Published:");
} catch (Exception ex) { Logger.Warning(string.Format("Informazioni generali non riconosciute nel file {0}\r\n{1}", filePathName, ex.Message)); return null; }
// Data del racconto
try {
CultureInfo culture = CultureInfo.CreateSpecificCulture("it-IT");
DateTimeStyles styles = DateTimeStyles.None;
if (!DateTime.TryParse(infoPublished.Trim(), culture, styles, out creato)) {
Logger.Error($"Old Milu - Import - No data - {filePathName}");
}
} catch (Exception ex) { Logger.Warning(string.Format("Data non riconosciuta nel file {0}\r\n{1}", filePathName, ex.Message)); return null; }
// Notes (comuni fra tutti i racconti):
try {
notes = new List<Data.cNota>();
notes.Add(new Data.cNota("SrcFile", System.IO.Path.GetFileName(filePathName)));
notes.Add(new Data.cNota("infoCategories", infoCategories));
notes.Add(new Data.cNota("infoCharacters", infoCharacters));
notes.Add(new Data.cNota("infoGenres", infoGenres));
notes.Add(new Data.cNota("infoWarnings", infoWarnings));
notes.Add(new Data.cNota("infoChallenges", infoChallenges));
notes.Add(new Data.cNota("infoSeries", infoSeries));
notes.Add(new Data.cNota("infoChapters", infoChapters));
notes.Add(new Data.cNota("infoCompleted", infoCompleted));
notes.Add(new Data.cNota("infoWordCount", infoWordCount));
notes.Add(new Data.cNota("infoRead", infoRead));
notes.Add(new Data.cNota("infoPublished", infoPublished));
} catch (Exception ex) { Logger.Warning(string.Format("Notes non riconosciute nel file {0}\r\n{1}", filePathName, ex.Message)); return null; }
#endregion
#endregion
// I vari capitoli dei racconti sono descritti dai nodi <div class='chaptertitle'> e <div class='chapter'> in successione.
try {
HtmlNodeCollection capTitles = root.SelectNodes("//div[@class='chaptertitle']");
HtmlNodeCollection capText = root.SelectNodes("//div[@class='chapter']");
if ((capTitles.Count > 0) && (capTitles.Count == capText.Count)) {
// Trovato capitoli congruenti
for (int i = 0; i < capTitles.Count; i++) {
// Creo i racconti:
rac = new cRac();
rac.Title = titolo;
rac.SubTitle = cTools.FormattaTesto(capTitles[i].InnerText.Trim().Replace($"by {autore}","").Trim(),true);
rac.Creato = creato;
rac.Updated = DateTime.Now;
//rac.Trama = cTools.FormattaTesto(capText[i].InnerHtml.Trim(), false);
rac.Trama = cTools.FormattaTesto(capText[i].InnerHtml.Trim(), true);
rac.Voto = "Non Letto";
rac.Autori = new List<string>();
rac.Autori.Add(autore);
rac.Tags = tags;
rac.Origini = origini;
rac.Notes = notes;
// Inserisco il racconto nella collezione
if (rac.Validate()) { racs.Add(rac); }
}
} else {
// Capitoli non trovati o non congruenti
Logger.Error($"Old Milu - Import - No Capitoli - {filePathName}");
return null;
}
} catch (Exception ex) { Logger.Warning(string.Format("Fallita estrazione capitolo {0}\r\n{1}", filePathName, ex.Message)); return null; }
Logger.Information($"Terminato estrazione racconti dal file: {filePathName}");
} catch (Exception ex) {
Logger.Error($"Old Milu - Import Error - {FileFullPath}: {ex.Message}");
Console.WriteLine($"Old Milu - Import Error - {FileFullPath}: {ex.Message}");
return null;
}
return racs;
}
protected string TxtExtract(string src, string start, string end) {
int nstart = src.LastIndexOf(start);
int nstop = src.LastIndexOf(end);
int offset = start.Length;
return src.Substring(nstart + offset, nstop - nstart - offset).Trim();
}
}//class Service_Import_Milu
}//namespace A69WD.Service