Compare commits

...

8 Commits

Author SHA1 Message Date
a8f0f1af15 More API Requests 2023-08-26 02:43:24 +02:00
0cf3a95f58 cachedPublications 2023-08-26 02:42:57 +02:00
a89a526fda Default language GetChapters: en 2023-08-26 02:42:31 +02:00
4d1e43e7b3 Job: add Id 2023-08-26 02:40:24 +02:00
7614f9aad3 Add User Agent to MangaConnectors 2023-08-26 01:50:31 +02:00
97c0e42512 Handle first requests, add parameter parser 2023-08-26 01:47:36 +02:00
565bc0775d Add Connectors to Tranga 2023-08-26 01:47:15 +02:00
e6a3fa2899 public GetPublications 2023-08-26 01:46:36 +02:00
13 changed files with 213 additions and 30 deletions

View File

@ -9,8 +9,9 @@ public abstract class GlobalBase
{ {
protected Logger? logger { get; init; } protected Logger? logger { get; init; }
protected TrangaSettings settings { get; init; } protected TrangaSettings settings { get; init; }
private HashSet<NotificationConnector> notificationConnectors { get; init; } protected HashSet<NotificationConnector> notificationConnectors { get; init; }
private HashSet<LibraryConnector> libraryConnectors { get; init; } protected HashSet<LibraryConnector> libraryConnectors { get; init; }
protected List<Publication> cachedPublications { get; init; }
protected GlobalBase(GlobalBase clone) protected GlobalBase(GlobalBase clone)
{ {
@ -18,6 +19,7 @@ public abstract class GlobalBase
this.settings = clone.settings; this.settings = clone.settings;
this.notificationConnectors = clone.notificationConnectors; this.notificationConnectors = clone.notificationConnectors;
this.libraryConnectors = clone.libraryConnectors; this.libraryConnectors = clone.libraryConnectors;
this.cachedPublications = clone.cachedPublications;
} }
protected GlobalBase(Logger? logger, TrangaSettings settings) protected GlobalBase(Logger? logger, TrangaSettings settings)
@ -26,6 +28,7 @@ public abstract class GlobalBase
this.settings = settings; this.settings = settings;
this.notificationConnectors = settings.LoadNotificationConnectors(); this.notificationConnectors = settings.LoadNotificationConnectors();
this.libraryConnectors = settings.LoadLibraryConnectors(); this.libraryConnectors = settings.LoadLibraryConnectors();
this.cachedPublications = new();
} }
protected void Log(string message) protected void Log(string message)

View File

@ -1,4 +1,5 @@
using Tranga.MangaConnectors; using System.Text;
using Tranga.MangaConnectors;
namespace Tranga.Jobs; namespace Tranga.Jobs;
@ -10,6 +11,11 @@ public class DownloadChapter : Job
{ {
this.chapter = chapter; this.chapter = chapter;
} }
protected override string GetId()
{
return Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Concat(this.GetType().ToString(), chapter.parentPublication.internalId, chapter.chapterNumber)));
}
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal() protected override IEnumerable<Job> ExecuteReturnSubTasksInternal()
{ {

View File

@ -1,4 +1,5 @@
using Tranga.MangaConnectors; using System.Text;
using Tranga.MangaConnectors;
namespace Tranga.Jobs; namespace Tranga.Jobs;
@ -11,6 +12,11 @@ public class DownloadNewChapters : Job
this.publication = publication; this.publication = publication;
} }
protected override string GetId()
{
return Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Concat(this.GetType().ToString(), publication.internalId)));
}
protected override IEnumerable<Job> ExecuteReturnSubTasksInternal() protected override IEnumerable<Job> ExecuteReturnSubTasksInternal()
{ {
Chapter[] chapters = mangaConnector.GetNewChapters(publication); Chapter[] chapters = mangaConnector.GetNewChapters(publication);

View File

@ -10,6 +10,7 @@ public abstract class Job : GlobalBase
public TimeSpan? recurrenceTime { get; set; } public TimeSpan? recurrenceTime { get; set; }
public DateTime? lastExecution { get; private set; } public DateTime? lastExecution { get; private set; }
public DateTime nextExecution => NextExecution(); public DateTime nextExecution => NextExecution();
public string id => GetId();
public Job(GlobalBase clone, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null) : base(clone) public Job(GlobalBase clone, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null) : base(clone)
{ {
@ -21,6 +22,8 @@ public abstract class Job : GlobalBase
this.recurrenceTime = recurrenceTime; this.recurrenceTime = recurrenceTime;
} }
protected abstract string GetId();
public Job(GlobalBase clone, MangaConnector connector, ProgressToken progressToken, bool recurring = false, TimeSpan? recurrenceTime = null) : base(clone) public Job(GlobalBase clone, MangaConnector connector, ProgressToken progressToken, bool recurring = false, TimeSpan? recurrenceTime = null) : base(clone)
{ {
this.mangaConnector = connector; this.mangaConnector = connector;

View File

@ -1,11 +1,10 @@
using System.Runtime.CompilerServices; using Tranga.MangaConnectors;
using Tranga.MangaConnectors;
namespace Tranga.Jobs; namespace Tranga.Jobs;
public class JobBoss : GlobalBase public class JobBoss : GlobalBase
{ {
private HashSet<Job> jobs { get; init; } public HashSet<Job> jobs { get; init; }
private Dictionary<MangaConnector, Queue<Job>> mangaConnectorJobQueue { get; init; } private Dictionary<MangaConnector, Queue<Job>> mangaConnectorJobQueue { get; init; }
public JobBoss(GlobalBase clone) : base(clone) public JobBoss(GlobalBase clone) : base(clone)

View File

@ -1,4 +1,5 @@
using System.Net; using System.Net;
using System.Net.Http.Headers;
namespace Tranga.MangaConnectors; namespace Tranga.MangaConnectors;
@ -6,7 +7,14 @@ internal class DownloadClient : GlobalBase
{ {
private static readonly HttpClient Client = new() private static readonly HttpClient Client = new()
{ {
Timeout = TimeSpan.FromSeconds(60) Timeout = TimeSpan.FromSeconds(60),
DefaultRequestHeaders =
{
UserAgent =
{
new ProductInfoHeaderValue("Tranga", "0.1")
}
}
}; };
private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit; private readonly Dictionary<byte, DateTime> _lastExecutedRateLimit;

View File

@ -30,7 +30,7 @@ public abstract class MangaConnector : GlobalBase
/// </summary> /// </summary>
/// <param name="publicationTitle">Search-Query</param> /// <param name="publicationTitle">Search-Query</param>
/// <returns>Publications matching the query</returns> /// <returns>Publications matching the query</returns>
protected abstract Publication[] GetPublications(string publicationTitle = ""); public abstract Publication[] GetPublications(string publicationTitle = "");
/// <summary> /// <summary>
/// Returns all Chapters of the publication in the provided language. /// Returns all Chapters of the publication in the provided language.
@ -39,7 +39,7 @@ public abstract class MangaConnector : GlobalBase
/// <param name="publication">Publication to get Chapters for</param> /// <param name="publication">Publication to get Chapters for</param>
/// <param name="language">Language of the Chapters</param> /// <param name="language">Language of the Chapters</param>
/// <returns>Array of Chapters matching Publication and Language</returns> /// <returns>Array of Chapters matching Publication and Language</returns>
public abstract Chapter[] GetChapters(Publication publication, string language = ""); public abstract Chapter[] GetChapters(Publication publication, string language="en");
/// <summary> /// <summary>
/// Updates the available Chapters of a Publication /// Updates the available Chapters of a Publication

View File

@ -31,7 +31,7 @@ public class MangaDex : MangaConnector
}, clone); }, clone);
} }
protected override Publication[] GetPublications(string publicationTitle = "") public override Publication[] GetPublications(string publicationTitle = "")
{ {
Log($"Searching Publications. Term=\"{publicationTitle}\""); Log($"Searching Publications. Term=\"{publicationTitle}\"");
const int limit = 100; //How many values we want returned at once const int limit = 100; //How many values we want returned at once
@ -146,11 +146,12 @@ public class MangaDex : MangaConnector
} }
} }
Log($"Retrieved {publications.Count} publications."); cachedPublications.AddRange(publications);
Log($"Retrieved {publications.Count} publications. Term=\"{publicationTitle}\"");
return publications.ToArray(); return publications.ToArray();
} }
public override Chapter[] GetChapters(Publication publication, string language = "") public override Chapter[] GetChapters(Publication publication, string language="en")
{ {
Log($"Getting chapters {publication}"); Log($"Getting chapters {publication}");
const int limit = 100; //How many values we want returned at once const int limit = 100; //How many values we want returned at once

View File

@ -19,7 +19,7 @@ public class MangaKatana : MangaConnector
}, clone); }, clone);
} }
protected override Publication[] GetPublications(string publicationTitle = "") public override Publication[] GetPublications(string publicationTitle = "")
{ {
Log($"Searching Publications. Term=\"{publicationTitle}\""); Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower(); string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*").Where(m => m.Value.Length > 0)).ToLower();
@ -39,7 +39,8 @@ public class MangaKatana : MangaConnector
} }
Publication[] publications = ParsePublicationsFromHtml(requestResult.result); Publication[] publications = ParsePublicationsFromHtml(requestResult.result);
Log($"Retrieved {publications.Length} publications."); cachedPublications.AddRange(publications);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications; return publications;
} }
@ -137,7 +138,7 @@ public class MangaKatana : MangaConnector
year, originalLanguage, status, publicationId); year, originalLanguage, status, publicationId);
} }
public override Chapter[] GetChapters(Publication publication, string language = "") public override Chapter[] GetChapters(Publication publication, string language="en")
{ {
Log($"Getting chapters {publication}"); Log($"Getting chapters {publication}");
string requestUrl = $"https://mangakatana.com/manga/{publication.publicationId}"; string requestUrl = $"https://mangakatana.com/manga/{publication.publicationId}";

View File

@ -19,7 +19,7 @@ public class Manganato : MangaConnector
}, clone); }, clone);
} }
protected override Publication[] GetPublications(string publicationTitle = "") public override Publication[] GetPublications(string publicationTitle = "")
{ {
Log($"Searching Publications. Term=\"{publicationTitle}\""); Log($"Searching Publications. Term=\"{publicationTitle}\"");
string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*")).ToLower(); string sanitizedTitle = string.Join('_', Regex.Matches(publicationTitle, "[A-z]*")).ToLower();
@ -30,7 +30,8 @@ public class Manganato : MangaConnector
return Array.Empty<Publication>(); return Array.Empty<Publication>();
Publication[] publications = ParsePublicationsFromHtml(requestResult.result); Publication[] publications = ParsePublicationsFromHtml(requestResult.result);
Log($"Retrieved {publications.Length} publications."); cachedPublications.AddRange(publications);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications; return publications;
} }
@ -125,7 +126,7 @@ public class Manganato : MangaConnector
year, originalLanguage, status, publicationId); year, originalLanguage, status, publicationId);
} }
public override Chapter[] GetChapters(Publication publication, string language = "") public override Chapter[] GetChapters(Publication publication, string language="en")
{ {
Log($"Getting chapters {publication}"); Log($"Getting chapters {publication}");
string requestUrl = $"https://chapmanganato.com/{publication.publicationId}"; string requestUrl = $"https://chapmanganato.com/{publication.publicationId}";

View File

@ -69,7 +69,7 @@ public class Mangasee : MangaConnector
}); });
} }
protected override Publication[] GetPublications(string publicationTitle = "") public override Publication[] GetPublications(string publicationTitle = "")
{ {
Log($"Searching Publications. Term=\"{publicationTitle}\""); Log($"Searching Publications. Term=\"{publicationTitle}\"");
string requestUrl = $"https://mangasee123.com/_search.php"; string requestUrl = $"https://mangasee123.com/_search.php";
@ -78,7 +78,10 @@ public class Mangasee : MangaConnector
if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300) if ((int)requestResult.statusCode < 200 || (int)requestResult.statusCode >= 300)
return Array.Empty<Publication>(); return Array.Empty<Publication>();
return ParsePublicationsFromHtml(requestResult.result, publicationTitle); Publication[] publications = ParsePublicationsFromHtml(requestResult.result, publicationTitle);
cachedPublications.AddRange(publications);
Log($"Retrieved {publications.Length} publications. Term=\"{publicationTitle}\"");
return publications;
} }
private Publication[] ParsePublicationsFromHtml(Stream html, string publicationTitle) private Publication[] ParsePublicationsFromHtml(Stream html, string publicationTitle)
@ -212,7 +215,7 @@ public class Mangasee : MangaConnector
} }
} }
public override Chapter[] GetChapters(Publication publication, string language = "") public override Chapter[] GetChapters(Publication publication, string language="en")
{ {
Log($"Getting chapters {publication}"); Log($"Getting chapters {publication}");
XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{publication.publicationId}.xml"); XDocument doc = XDocument.Load($"https://mangasee123.com/rss/{publication.publicationId}.xml");

View File

@ -1,7 +1,10 @@
using System.Net; using System.Net;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json; using Newtonsoft.Json;
using Tranga.Jobs;
using Tranga.MangaConnectors;
namespace Tranga; namespace Tranga;
@ -32,10 +35,7 @@ public class Server : GlobalBase
Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}"); Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}");
Task t = new(() => Task t = new(() =>
{ {
if(context.Request.HttpMethod == "OPTIONS") HandleRequest(context);
SendResponse(HttpStatusCode.OK, context.Response);
else
HandleRequest(context);
}); });
t.Start(); t.Start();
} }
@ -45,15 +45,138 @@ public class Server : GlobalBase
{ {
HttpListenerRequest request = context.Request; HttpListenerRequest request = context.Request;
HttpListenerResponse response = context.Response; HttpListenerResponse response = context.Response;
if(request.HttpMethod == "OPTIONS")
SendResponse(HttpStatusCode.OK, context.Response);
if(request.Url!.LocalPath.Contains("favicon")) if(request.Url!.LocalPath.Contains("favicon"))
SendResponse(HttpStatusCode.NoContent, response); SendResponse(HttpStatusCode.NoContent, response);
switch (request.HttpMethod)
{
case "GET":
HandleGet(request, request.InputStream, response);
break;
case "POST":
HandlePost(request, request.InputStream, response);
break;
case "DELETE":
HandleDelete(request, request.InputStream, response);
break;
default:
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
}
private Dictionary<string, string> GetRequestVariables(string query)
{
Dictionary<string, string> ret = new();
Regex queryRex = new (@"\?{1}&?([A-z0-9-=]+=[A-z0-9-=]+)+(&[A-z0-9-=]+=[A-z0-9-=]+)*");
if (!queryRex.IsMatch(query))
return ret;
query = query.Substring(1);
foreach (string kvpair in query.Split('&').Where(str => str.Length >= 3))
{
string var = kvpair.Split('=')[0];
string val = Regex.Replace(kvpair.Substring(var.Length + 1), "%20", " ");
val = Regex.Replace(val, "%[0-9]{2}", "");
ret.Add(var, val);
}
return ret;
}
private void HandleGet(HttpListenerRequest request, Stream content, HttpListenerResponse response)
{
Dictionary<string, string> requestVariables = GetRequestVariables(request.Url!.Query);
string? connectorName, title, internalId, jobId;
MangaConnector connector;
Publication publication;
Job job;
string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
switch (path)
{
case "Connectors":
SendResponse(HttpStatusCode.OK, response, _parent.GetConnectors().Select(connector => connector.name).ToArray());
break;
case "Publications/FromConnector":
if (!requestVariables.TryGetValue("connector", out connectorName) ||
!requestVariables.TryGetValue("title", out title) ||
_parent.GetConnector(connectorName) is null)
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
connector = _parent.GetConnector(connectorName)!;
SendResponse(HttpStatusCode.OK, response, connector.GetPublications(title));
break;
case "Publications/Chapters":
if(!requestVariables.TryGetValue("connector", out connectorName) ||
!requestVariables.TryGetValue("internalId", out internalId) ||
_parent.GetConnector(connectorName) is null ||
_parent.GetPublicationById(internalId) is null)
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
connector = _parent.GetConnector(connectorName)!;
publication = (Publication)_parent.GetPublicationById(internalId)!;
SendResponse(HttpStatusCode.OK, response, connector.GetChapters(publication));
break;
case "Tasks":
if (!requestVariables.TryGetValue("jobId", out jobId))
{
if(!_parent._jobBoss.jobs.Any(jjob => jjob.id == jobId))
SendResponse(HttpStatusCode.BadRequest, response);
else
SendResponse(HttpStatusCode.OK, response, _parent._jobBoss.jobs.First(jjob => jjob.id == jobId));
break;
}
SendResponse(HttpStatusCode.OK, response, _parent._jobBoss.jobs);
break;
case "Tasks/Progress":
if (!requestVariables.TryGetValue("jobId", out jobId))
{
if(!_parent._jobBoss.jobs.Any(jjob => jjob.id == jobId))
SendResponse(HttpStatusCode.BadRequest, response);
else
SendResponse(HttpStatusCode.OK, response, _parent._jobBoss.jobs.First(jjob => jjob.id == jobId).progressToken);
break;
}
SendResponse(HttpStatusCode.OK, response, _parent._jobBoss.jobs.Select(jjob => jjob.progressToken));
break;
case "Tasks/Running":
SendResponse(HttpStatusCode.OK, response, _parent._jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Running));
break;
case "Tasks/Waiting":
SendResponse(HttpStatusCode.OK, response, _parent._jobBoss.jobs.Where(jjob => jjob.progressToken.state is ProgressToken.State.Standby));
break;
case "Settings":
SendResponse(HttpStatusCode.OK, response, settings);
break;
case "NotificationConnectors":
SendResponse(HttpStatusCode.OK, response, notificationConnectors);
break;
case "LibraryConnectors":
SendResponse(HttpStatusCode.OK, response, libraryConnectors);
break;
default:
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
}
private void HandlePost(HttpListenerRequest request, Stream content, HttpListenerResponse response)
{
}
private void HandleDelete(HttpListenerRequest request, Stream content, HttpListenerResponse response)
{
SendResponse(HttpStatusCode.NotFound, response);
} }
private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null) private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
{ {
//logger?.WriteLine(this.GetType().ToString(), $"Sending response: {statusCode}"); Log($"Response: {statusCode} {content}");
response.StatusCode = (int)statusCode; response.StatusCode = (int)statusCode;
response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With"); response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE"); response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");

View File

@ -1,22 +1,51 @@
using Logging; using Logging;
using Tranga.Jobs; using Tranga.Jobs;
using Tranga.MangaConnectors;
namespace Tranga; namespace Tranga;
public partial class Tranga : GlobalBase public partial class Tranga : GlobalBase
{ {
public bool keepRunning; public bool keepRunning;
private JobBoss _jobBoss; public JobBoss _jobBoss;
private Server server; private Server server;
private HashSet<MangaConnector> connectors;
public Tranga(Logger? logger, TrangaSettings settings) : base(logger, settings) public Tranga(Logger? logger, TrangaSettings settings) : base(logger, settings)
{ {
keepRunning = true; keepRunning = true;
_jobBoss = new(this); _jobBoss = new(this);
connectors = new HashSet<MangaConnector>()
{
new Manganato(this),
new Mangasee(this),
new MangaDex(this),
new MangaKatana(this)
};
StartJobBoss(); StartJobBoss();
this.server = new Server(this); this.server = new Server(this);
} }
public MangaConnector? GetConnector(string name)
{
foreach(MangaConnector mc in connectors)
if (mc.name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
return mc;
return null;
}
public IEnumerable<MangaConnector> GetConnectors()
{
return connectors;
}
public Publication? GetPublicationById(string internalId)
{
if (cachedPublications.Exists(publication => publication.internalId == internalId))
return cachedPublications.First(publication => publication.internalId == internalId);
return null;
}
private void StartJobBoss() private void StartJobBoss()
{ {
Thread t = new (() => Thread t = new (() =>