Compare commits

..

15 Commits

Author SHA1 Message Date
14ba71005f CheckJobs combined cancelled and completed checks,
added standby check
2023-09-02 16:16:00 +02:00
22c4c0eb2c Fixed GetJobsLike, for empty publication, but existing chapter 2023-09-02 16:15:06 +02:00
44f8d369c3 Added AddJobs to JobBoss 2023-09-02 16:14:36 +02:00
c0e6da144e Changed Job.ExecuteNow to ExecutionEnqueue
Instead of replacing progressToken, change Increments based in completed increments
2023-09-02 16:14:21 +02:00
51a1ae72ca Added parentJobId for deserialization
When creating Jobs with null as recurrence time, set it to zero
Job.NextExecution() removed the recurrence check
2023-09-02 16:12:10 +02:00
79bbc92467 Added lastExecution time on jobs.json parse 2023-09-02 15:05:15 +02:00
ae5be31c89 Fixed Jobs/StartNow 2023-09-02 14:49:31 +02:00
eebe25a378 Added check if jobQueue is empty 2023-09-02 14:46:38 +02:00
0f3da4ec81 Added check to read/write jobs.json if file is in use
Write jobs.json on change
2023-09-02 14:46:13 +02:00
0b77dc1172 Added ProgressToken state Cancelled 2023-09-02 14:45:46 +02:00
37cf47bc17 Reduced CheckJobs timer to 100ms 2023-09-02 14:45:02 +02:00
4cce2e04cb Renamed Job.Reset to ResetProgress 2023-09-02 14:13:30 +02:00
5465ac4e5c Removed DELETE Jobs/DownloadChapter and Jobs/MonitorManga. Can both be reached with DELETE Jobs (jobId)
Added POST Jobs/Cancel
CancelJob and RemoveJob cancels/removes subJobs
2023-09-02 14:13:15 +02:00
dd4d5a81ee Fix JobId variable in API requests 2023-09-02 14:11:44 +02:00
a05e1914e3 Log output changes 2023-09-02 14:11:11 +02:00
9 changed files with 146 additions and 81 deletions

View File

@ -6,8 +6,13 @@ namespace Tranga.Jobs;
public class DownloadChapter : Job
{
public Chapter chapter { get; init; }
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, DateTime lastExecution, string? parentJobId = null) : base(clone, connector, lastExecution, parentJobId: parentJobId)
{
this.chapter = chapter;
}
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter) : base(clone, connector)
public DownloadChapter(GlobalBase clone, MangaConnector connector, Chapter chapter, string? parentJobId = null) : base(clone, connector, parentJobId: parentJobId)
{
this.chapter = chapter;
}

View File

@ -6,8 +6,15 @@ namespace Tranga.Jobs;
public class DownloadNewChapters : Job
{
public Manga manga { get; init; }
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, DateTime lastExecution,
bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null) : base(clone, connector, lastExecution, recurring,
recurrence, parentJobId)
{
this.manga = manga;
}
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, bool recurring = false, TimeSpan? recurrence = null) : base (clone, connector, recurring, recurrence)
public DownloadNewChapters(GlobalBase clone, MangaConnector connector, Manga manga, bool recurring = false, TimeSpan? recurrence = null, string? parentJobId = null) : base (clone, connector, recurring, recurrence, parentJobId)
{
this.manga = manga;
}
@ -26,13 +33,13 @@ public class DownloadNewChapters : Job
{
Chapter[] chapters = mangaConnector.GetNewChapters(manga);
this.progressToken.increments = chapters.Length;
List<Job> subJobs = new();
List<Job> jobs = new();
foreach (Chapter chapter in chapters)
{
DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter);
subJobs.Add(downloadChapterJob);
DownloadChapter downloadChapterJob = new(this, this.mangaConnector, chapter, parentJobId: this.id);
jobs.Add(downloadChapterJob);
}
progressToken.Complete();
return subJobs;
return jobs;
}
}

View File

@ -11,8 +11,10 @@ public abstract class Job : GlobalBase
public DateTime? lastExecution { get; private set; }
public DateTime nextExecution => NextExecution();
public string id => GetId();
internal IEnumerable<Job>? subJobs { get; private set; }
public string? parentJobId { get; init; }
public Job(GlobalBase clone, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null) : base(clone)
internal Job(GlobalBase clone, MangaConnector connector, bool recurring = false, TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
{
this.mangaConnector = connector;
this.progressToken = new ProgressToken(0);
@ -21,57 +23,70 @@ public abstract class Job : GlobalBase
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
else if(recurring && recurrenceTime is not null)
this.lastExecution = DateTime.Now.Subtract((TimeSpan)recurrenceTime);
this.recurrenceTime = recurrenceTime;
this.recurrenceTime = recurrenceTime ?? TimeSpan.Zero;
this.parentJobId = parentJobId;
}
internal Job(GlobalBase clone, MangaConnector connector, DateTime lastExecution, bool recurring = false,
TimeSpan? recurrenceTime = null, string? parentJobId = null) : base(clone)
{
this.mangaConnector = connector;
this.progressToken = new ProgressToken(0);
this.recurring = recurring;
if (recurring && recurrenceTime is null)
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
this.lastExecution = lastExecution;
this.recurrenceTime = recurrenceTime ?? TimeSpan.Zero;
this.parentJobId = parentJobId;
}
protected abstract string GetId();
public Job(GlobalBase clone, MangaConnector connector, ProgressToken progressToken, bool recurring = false, TimeSpan? recurrenceTime = null) : base(clone)
public void AddSubJob(Job job)
{
this.mangaConnector = connector;
this.progressToken = progressToken;
this.recurring = recurring;
if (recurring && recurrenceTime is null)
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
this.recurrenceTime = recurrenceTime;
}
public Job(GlobalBase clone, MangaConnector connector, int taskIncrements, bool recurring = false, TimeSpan? recurrenceTime = null) : base(clone)
{
this.mangaConnector = connector;
this.progressToken = new ProgressToken(taskIncrements);
this.recurring = recurring;
if (recurring && recurrenceTime is null)
throw new ArgumentException("If recurrence is set to true, a recurrence time has to be provided.");
this.recurrenceTime = recurrenceTime;
subJobs ??= new List<Job>();
subJobs = subJobs.Append(job);
}
private DateTime NextExecution()
{
if(recurring && recurrenceTime.HasValue && lastExecution.HasValue)
if(recurrenceTime.HasValue && lastExecution.HasValue)
return lastExecution.Value.Add(recurrenceTime.Value);
if(recurring && recurrenceTime.HasValue && !lastExecution.HasValue)
if(recurrenceTime.HasValue && !lastExecution.HasValue)
return DateTime.Now;
return DateTime.MaxValue;
}
public void Reset()
public void ResetProgress()
{
this.progressToken = new ProgressToken(this.progressToken.increments);
this.progressToken.increments = this.progressToken.increments - this.progressToken.incrementsCompleted;
this.lastExecution = DateTime.Now;
}
public void ExecutionEnqueue()
{
this.progressToken.increments = this.progressToken.increments - this.progressToken.incrementsCompleted;
this.lastExecution = recurrenceTime is not null ? DateTime.Now.Subtract((TimeSpan)recurrenceTime) : DateTime.UnixEpoch;
this.progressToken.Standby();
}
public void Cancel()
{
Log($"Cancelling {this}");
this.progressToken.cancellationRequested = true;
this.progressToken.Complete();
this.progressToken.Cancel();
this.lastExecution = DateTime.Now;
if(subJobs is not null)
foreach(Job subJob in subJobs)
subJob.Cancel();
}
public IEnumerable<Job> ExecuteReturnSubTasks()
{
progressToken.Start();
IEnumerable<Job> ret = ExecuteReturnSubTasksInternal();
subJobs = ExecuteReturnSubTasksInternal();
lastExecution = DateTime.Now;
return ret;
return subJobs;
}
protected abstract IEnumerable<Job> ExecuteReturnSubTasksInternal();

View File

@ -11,7 +11,11 @@ public class JobBoss : GlobalBase
public JobBoss(GlobalBase clone, HashSet<MangaConnector> connectors) : base(clone)
{
if (File.Exists(settings.jobsFilePath))
{
this.jobs = JsonConvert.DeserializeObject<HashSet<Job>>(File.ReadAllText(settings.jobsFilePath), new JobJsonConverter(this, new MangaConnectorJsonConverter(this, connectors)))!;
foreach (Job job in this.jobs)
this.jobs.FirstOrDefault(jjob => jjob.id == job.parentJobId)?.AddSubJob(job);
}
else
this.jobs = new();
foreach (DownloadNewChapters ncJob in this.jobs.Where(job => job is DownloadNewChapters))
@ -29,10 +33,16 @@ public class JobBoss : GlobalBase
{
Log($"Added {job}");
this.jobs.Add(job);
File.WriteAllText(settings.jobsFilePath, JsonConvert.SerializeObject(this.jobs));
ExportJobsList();
}
}
public void AddJobs(IEnumerable<Job> jobsToAdd)
{
foreach (Job job in jobsToAdd)
AddJob(job);
}
public bool ContainsJobLike(Job job)
{
if (job is DownloadChapter dcJob)
@ -51,13 +61,17 @@ public class JobBoss : GlobalBase
Log($"Removing {job}");
job.Cancel();
this.jobs.Remove(job);
if(job.subJobs is not null)
RemoveJobs(job.subJobs);
ExportJobsList();
}
public void RemoveJobs(IEnumerable<Job> jobsToRemove)
public void RemoveJobs(IEnumerable<Job?> jobsToRemove)
{
Log($"Removing {jobsToRemove.Count()} jobs.");
foreach (Job job in jobsToRemove)
RemoveJob(job);
foreach (Job? job in jobsToRemove)
if(job is not null)
RemoveJob(job);
}
public IEnumerable<Job> GetJobsLike(string? connectorName = null, string? internalId = null, string? chapterNumber = null)
@ -87,7 +101,10 @@ public class JobBoss : GlobalBase
public IEnumerable<Job> GetJobsLike(MangaConnector? mangaConnector = null, Manga? publication = null,
Chapter? chapter = null)
{
return GetJobsLike(mangaConnector?.name, publication?.internalId, chapter?.chapterNumber);
if (chapter is not null)
return GetJobsLike(mangaConnector?.name, chapter.Value.parentManga.internalId, chapter?.chapterNumber);
else
return GetJobsLike(mangaConnector?.name, publication?.internalId);
}
public Job? GetJobById(string jobId)
@ -122,6 +139,7 @@ public class JobBoss : GlobalBase
Queue<Job> connectorJobQueue = mangaConnectorJobQueue[job.mangaConnector];
if(!connectorJobQueue.Contains(job))
connectorJobQueue.Enqueue(job);
job.ExecutionEnqueue();
}
public void AddJobsToQueue(IEnumerable<Job> jobs)
@ -130,20 +148,42 @@ public class JobBoss : GlobalBase
AddJobToQueue(job);
}
public void ExportJobsList()
{
Log($"Exporting {settings.jobsFilePath}");
while(IsFileInUse(settings.jobsFilePath))
Thread.Sleep(10);
File.WriteAllText(settings.jobsFilePath, JsonConvert.SerializeObject(this.jobs));
}
public void CheckJobs()
{
foreach (Job job in jobs.Where(job => job.nextExecution < DateTime.Now && !QueueContainsJob(job)).OrderBy(job => job.nextExecution))
AddJobToQueue(job);
foreach (Queue<Job> jobQueue in mangaConnectorJobQueue.Values)
{
if(jobQueue.Count < 1)
continue;
Job queueHead = jobQueue.Peek();
if (queueHead.progressToken.state is ProgressToken.State.Complete)
if (queueHead.progressToken.state is ProgressToken.State.Complete or ProgressToken.State.Cancelled)
{
if(queueHead.recurring)
queueHead.Reset();
switch (queueHead)
{
case DownloadChapter:
RemoveJob(queueHead);
break;
case DownloadNewChapters:
if(queueHead.recurring)
queueHead.progressToken.Complete();
break;
}
jobQueue.Dequeue();
}else if(queueHead.progressToken.state is ProgressToken.State.Standby)
AddJobsToQueue(jobQueue.Peek().ExecuteReturnSubTasks());
}else if (queueHead.progressToken.state is ProgressToken.State.Standby)
{
Job[] subJobs = jobQueue.Peek().ExecuteReturnSubTasks().ToArray();
AddJobs(subJobs);
AddJobsToQueue(subJobs);
}
}
}
}

View File

@ -34,8 +34,10 @@ public class JobJsonConverter : JsonConverter
}
}))!,
jo.GetValue("manga")!.ToObject<Manga>(),
jo.GetValue("lastExecution")!.ToObject<DateTime>(),
jo.GetValue("recurring")!.Value<bool>(),
jo.GetValue("recurrenceTime")!.ToObject<TimeSpan?>());
jo.GetValue("recurrenceTime")!.ToObject<TimeSpan?>(),
jo.GetValue("parentJobId")!.Value<string?>());
}
if (jo.ContainsKey("chapter"))//DownloadChapter
@ -48,7 +50,9 @@ public class JobJsonConverter : JsonConverter
this._mangaConnectorJsonConverter
}
}))!,
jo.GetValue("chapter")!.ToObject<Chapter>());
jo.GetValue("chapter")!.ToObject<Chapter>(),
DateTime.UnixEpoch,
jo.GetValue("parentJobId")!.Value<string?>());
}
throw new Exception();

View File

@ -7,7 +7,7 @@ public class ProgressToken
public int incrementsCompleted { get; set; }
public float progress => GetProgress();
public enum State { Running, Complete, Standby }
public enum State { Running, Complete, Standby, Cancelled }
public State state { get; private set; }
public ProgressToken(int increments)
@ -15,7 +15,7 @@ public class ProgressToken
this.cancellationRequested = false;
this.increments = increments;
this.incrementsCompleted = 0;
this.state = State.Standby;
this.state = State.Complete;
}
private float GetProgress()
@ -32,6 +32,11 @@ public class ProgressToken
state = State.Complete;
}
public void Standby()
{
state = State.Standby;
}
public void Start()
{
state = State.Running;
@ -41,4 +46,9 @@ public class ProgressToken
{
state = State.Complete;
}
public void Cancel()
{
state = State.Cancelled;
}
}

View File

@ -60,7 +60,7 @@ internal class DownloadClient : GlobalBase
if(referrer is not null)
requestMessage.Headers.Referrer = new Uri(referrer);
_lastExecutedRateLimit[requestType] = DateTime.Now;
Log($"Requesting {requestType} {url}");
//Log($"Requesting {requestType} {url}");
response = Client.Send(requestMessage);
}
catch (HttpRequestException e)

View File

@ -45,7 +45,7 @@ public class Server : GlobalBase
try
{
HttpListenerContext context = this._listener.GetContext();
Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}");
//Log($"{context.Request.HttpMethod} {context.Request.Url} {context.Request.UserAgent}");
Task t = new(() =>
{
HandleRequest(context);
@ -211,9 +211,10 @@ public class Server : GlobalBase
private void HandlePost(HttpListenerRequest request, HttpListenerResponse response)
{
Dictionary<string, string> requestVariables = GetRequestVariables(request.Url!.Query);
string? connectorName, internalId;
string? connectorName, internalId, jobId;
MangaConnector connector;
Manga manga;
Job? job;
string path = Regex.Match(request.Url!.LocalPath, @"[A-z0-9]+(\/[A-z0-9]+)*").Value;
switch (path)
{
@ -248,8 +249,8 @@ public class Server : GlobalBase
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Jobs/StartNow":
if (!requestVariables.TryGetValue("jobId", out string? jobId) ||
!_parent.jobBoss.TryGetJobById(jobId, out Job? job))
if (!requestVariables.TryGetValue("jobId", out jobId) ||
!_parent.jobBoss.TryGetJobById(jobId, out job))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
@ -257,6 +258,16 @@ public class Server : GlobalBase
_parent.jobBoss.AddJobToQueue(job!);
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Jobs/Cancel":
if (!requestVariables.TryGetValue("jobId", out jobId) ||
!_parent.jobBoss.TryGetJobById(jobId, out job))
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
job!.Cancel();
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Settings/UpdateDownloadLocation":
if (!requestVariables.TryGetValue("downloadLocation", out string? downloadLocation) ||
!requestVariables.TryGetValue("moveFiles", out string? moveFilesStr) ||
@ -362,7 +373,7 @@ public class Server : GlobalBase
switch (path)
{
case "Jobs":
if (!requestVariables.TryGetValue("jobID", out string? jobId) ||
if (!requestVariables.TryGetValue("jobId", out string? jobId) ||
!_parent.jobBoss.TryGetJobById(jobId, out Job? job))
{
SendResponse(HttpStatusCode.BadRequest, response);
@ -371,33 +382,6 @@ public class Server : GlobalBase
_parent.jobBoss.RemoveJob(job!);
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Jobs/DownloadChapter":
if(!requestVariables.TryGetValue("connector", out connectorName) ||
!requestVariables.TryGetValue("internalId", out internalId) ||
!requestVariables.TryGetValue("chapterNumber", out string? chapterNumber) ||
_parent.GetConnector(connectorName) is null ||
_parent.GetPublicationById(internalId) is null)
{
SendResponse(HttpStatusCode.BadRequest, response);
break;
}
_parent.jobBoss.RemoveJobs(_parent.jobBoss.GetJobsLike(connectorName, internalId, chapterNumber));
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Jobs/MonitorManga":
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)!;
manga = (Manga)_parent.GetPublicationById(internalId)!;
_parent.jobBoss.RemoveJobs(_parent.jobBoss.GetJobsLike(connector, manga));
SendResponse(HttpStatusCode.Accepted, response);
break;
case "Jobs/DownloadNewChapters":
if(!requestVariables.TryGetValue("connector", out connectorName) ||
!requestVariables.TryGetValue("internalId", out internalId) ||
@ -441,7 +425,7 @@ public class Server : GlobalBase
private void SendResponse(HttpStatusCode statusCode, HttpListenerResponse response, object? content = null)
{
Log($"Response: {statusCode} {content}");
//Log($"Response: {statusCode} {content}");
response.StatusCode = (int)statusCode;
response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
response.AddHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");

View File

@ -65,7 +65,7 @@ public partial class Tranga : GlobalBase
while (keepRunning)
{
jobBoss.CheckJobs();
Thread.Sleep(1000);
Thread.Sleep(100);
}
});
t.Start();