using API.APIEndpointRecords;
using API.Schema;
using API.Schema.Contexts;
using API.Schema.Jobs;
using Asp.Versioning;
using log4net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using static Microsoft.AspNetCore.Http.StatusCodes;
// ReSharper disable InconsistentNaming
namespace API.Controllers;
[ApiVersion(2)]
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
public class JobController(PgsqlContext context, ILog Log) : Controller
{
///
/// Returns all Jobs
///
///
[HttpGet]
[ProducesResponseType(Status200OK, "application/json")]
public IActionResult GetAllJobs()
{
Job[] ret = context.Jobs.ToArray();
return Ok(ret);
}
///
/// Returns Jobs with requested Job-IDs
///
/// Array of Job-IDs
///
[HttpPost("WithIDs")]
[ProducesResponseType(Status200OK, "application/json")]
public IActionResult GetJobs([FromBody]string[] ids)
{
Job[] ret = context.Jobs.Where(job => ids.Contains(job.JobId)).ToArray();
return Ok(ret);
}
///
/// Get all Jobs in requested State
///
/// Requested Job-State
///
[HttpGet("State/{JobState}")]
[ProducesResponseType(Status200OK, "application/json")]
public IActionResult GetJobsInState(JobState JobState)
{
Job[] jobsInState = context.Jobs.Where(job => job.state == JobState).ToArray();
return Ok(jobsInState);
}
///
/// Returns all Jobs of requested Type
///
/// Requested Job-Type
///
[HttpGet("Type/{JobType}")]
[ProducesResponseType(Status200OK, "application/json")]
public IActionResult GetJobsOfType(JobType JobType)
{
Job[] jobsOfType = context.Jobs.Where(job => job.JobType == JobType).ToArray();
return Ok(jobsOfType);
}
///
/// Returns all Jobs of requested Type and State
///
/// Requested Job-Type
/// Requested Job-State
///
[HttpGet("TypeAndState/{JobType}/{JobState}")]
[ProducesResponseType(Status200OK, "application/json")]
public IActionResult GetJobsOfType(JobType JobType, JobState JobState)
{
Job[] jobsOfType = context.Jobs.Where(job => job.JobType == JobType && job.state == JobState).ToArray();
return Ok(jobsOfType);
}
///
/// Return Job with ID
///
/// Job-ID
///
/// Job with ID could not be found
[HttpGet("{JobId}")]
[ProducesResponseType(Status200OK, "application/json")]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetJob(string JobId)
{
Job? ret = context.Jobs.Find(JobId);
return (ret is not null) switch
{
true => Ok(ret),
false => NotFound()
};
}
///
/// Create a new DownloadAvailableChaptersJob
///
/// ID of Manga
/// Job-Configuration
/// Job-IDs
/// Could not find ToLibrary with ID
/// Could not find Manga with ID
/// Error during Database Operation
[HttpPut("DownloadAvailableChaptersJob/{MangaId}")]
[ProducesResponseType(Status201Created, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status500InternalServerError, "text/plain")]
public IActionResult CreateDownloadAvailableChaptersJob(string MangaId, [FromBody]DownloadAvailableChaptersJobRecord record)
{
if (context.Mangas.Find(MangaId) is not { } m)
return NotFound();
else
{
try
{
LocalLibrary? l = context.LocalLibraries.Find(record.localLibraryId);
if (l is null)
return BadRequest();
m.Library = l;
context.SaveChanges();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
Job retrieveChapters = new RetrieveChaptersJob(m, record.language, record.recurrenceTimeMs);
Job updateFilesDownloaded =
new UpdateChaptersDownloadedJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters]);
Job downloadChapters = new DownloadAvailableChaptersJob(m, record.recurrenceTimeMs, dependsOnJobs: [retrieveChapters, updateFilesDownloaded]);
Job UpdateCover = new UpdateCoverJob(m, record.recurrenceTimeMs, downloadChapters);
retrieveChapters.ParentJob = downloadChapters;
updateFilesDownloaded.ParentJob = retrieveChapters;
return AddJobs([retrieveChapters, downloadChapters, updateFilesDownloaded, UpdateCover]);
}
///
/// Create a new DownloadSingleChapterJob
///
/// ID of the Chapter
/// Job-IDs
/// Could not find Chapter with ID
/// Error during Database Operation
[HttpPut("DownloadSingleChapterJob/{ChapterId}")]
[ProducesResponseType(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status500InternalServerError, "text/plain")]
public IActionResult CreateNewDownloadChapterJob(string ChapterId)
{
if(context.Chapters.Find(ChapterId) is not { } c)
return NotFound();
Job job = new DownloadSingleChapterJob(c);
return AddJobs([job]);
}
///
/// Create a new UpdateChaptersDownloadedJob
///
/// ID of the Manga
/// Job-IDs
/// Could not find Manga with ID
/// Error during Database Operation
[HttpPut("UpdateFilesJob/{MangaId}")]
[ProducesResponseType(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateFilesDownloadedJob(string MangaId)
{
if(context.Mangas.Find(MangaId) is not { } m)
return NotFound();
Job job = new UpdateChaptersDownloadedJob(m, 0);
return AddJobs([job]);
}
///
/// Create a new UpdateMetadataJob for all Manga
///
/// Job-IDs
/// Error during Database Operation
[HttpPut("UpdateAllFilesJob")]
[ProducesResponseType(Status201Created, "application/json")]
[ProducesResponseType(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllFilesDownloadedJob()
{
List jobs = context.Mangas.Select(m => new UpdateChaptersDownloadedJob(m, 0, null, null)).ToList();
try
{
context.Jobs.AddRange(jobs);
context.SaveChanges();
return Created();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
///
/// Not Implemented: Create a new UpdateMetadataJob
///
/// ID of the Manga
/// Job-IDs
/// Could not find Manga with ID
/// Error during Database Operation
[HttpPut("UpdateMetadataJob/{MangaId}")]
[ProducesResponseType(Status201Created, "application/json")]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateMetadataJob(string MangaId)
{
return StatusCode(Status501NotImplemented);
}
///
/// Not Implemented: Create a new UpdateMetadataJob for all Manga
///
/// Job-IDs
/// Error during Database Operation
[HttpPut("UpdateAllMetadataJob")]
[ProducesResponseType(Status201Created, "application/json")]
[ProducesResponseType(Status500InternalServerError, "text/plain")]
public IActionResult CreateUpdateAllMetadataJob()
{
return StatusCode(Status501NotImplemented);
}
private IActionResult AddJobs(Job[] jobs)
{
try
{
context.Jobs.AddRange(jobs);
context.SaveChanges();
return new CreatedResult((string?)null, jobs.Select(j => j.JobId).ToArray());
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
///
/// Delete Job with ID and all children
///
/// Job-ID
///
/// Job could not be found
/// Error during Database Operation
[HttpDelete("{JobId}")]
[ProducesResponseType(Status200OK)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status500InternalServerError, "text/plain")]
public IActionResult DeleteJob(string JobId)
{
try
{
if(context.Jobs.Find(JobId) is not { } ret)
return NotFound();
context.Remove(ret);
context.SaveChanges();
return Ok();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
private IQueryable GetChildJobs(string parentJobId)
{
IQueryable children = context.Jobs.Where(j => j.ParentJobId == parentJobId);
foreach (Job child in children)
foreach (Job grandChild in GetChildJobs(child.JobId))
children.Append(grandChild);
return children;
}
///
/// Modify Job with ID
///
/// Job-ID
/// Fields to modify, set to null to keep previous value
/// Job modified
/// Malformed request
/// Job with ID not found
/// Error during Database Operation
[HttpPatch("{JobId}")]
[ProducesResponseType(Status202Accepted, "application/json")]
[ProducesResponseType(Status400BadRequest)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status500InternalServerError, "text/plain")]
public IActionResult ModifyJob(string JobId, [FromBody]ModifyJobRecord modifyJobRecord)
{
try
{
Job? ret = context.Jobs.Find(JobId);
if(ret is null)
return NotFound();
ret.RecurrenceMs = modifyJobRecord.RecurrenceMs ?? ret.RecurrenceMs;
ret.Enabled = modifyJobRecord.Enabled ?? ret.Enabled;
context.SaveChanges();
return new AcceptedResult(ret.JobId, ret);
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
///
/// Starts the Job with the requested ID
///
/// Job-ID
/// Start Jobs necessary for execution
/// Job started
/// Job with ID not found
/// Job was already running
/// Error during Database Operation
[HttpPost("{JobId}/Start")]
[ProducesResponseType(Status202Accepted)]
[ProducesResponseType(Status404NotFound)]
[ProducesResponseType(Status409Conflict)]
[ProducesResponseType(Status500InternalServerError, "text/plain")]
public IActionResult StartJob(string JobId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]bool startDependencies = false)
{
Job? ret = context.Jobs.Find(JobId);
if (ret is null)
return NotFound();
List dependencies = startDependencies ? ret.GetDependenciesAndSelf() : [ret];
try
{
if(dependencies.Any(d => d.state >= JobState.Running && d.state < JobState.Completed))
return new ConflictResult();
dependencies.ForEach(d =>
{
d.LastExecution = DateTime.UnixEpoch;
d.state = JobState.CompletedWaiting;
});
context.SaveChanges();
return Accepted();
}
catch (Exception e)
{
Log.Error(e);
return StatusCode(500, e.Message);
}
}
///
/// Stops the Job with the requested ID
///
/// Job-ID
/// NOT IMPLEMENTED
[HttpPost("{JobId}/Stop")]
[ProducesResponseType(Status501NotImplemented)]
public IActionResult StopJob(string JobId)
{
return StatusCode(Status501NotImplemented);
}
}