using System.Diagnostics.CodeAnalysis; using API.Controllers.DTOs; using API.Schema.MangaContext; using API.Workers; using Asp.Versioning; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.Net.Http.Headers; using static Microsoft.AspNetCore.Http.StatusCodes; using AltTitle = API.Controllers.DTOs.AltTitle; using Author = API.Controllers.DTOs.Author; using Chapter = API.Controllers.DTOs.Chapter; using Link = API.Controllers.DTOs.Link; using Manga = API.Controllers.DTOs.Manga; // ReSharper disable InconsistentNaming namespace API.Controllers; [ApiVersion(2)] [ApiController] [Route("v{v:apiVersion}/[controller]")] public class MangaController(MangaContext context) : Controller { /// /// Returns all cached /// /// exert of . Use for more information /// Error during Database Operation [HttpGet] [ProducesResponseType>(Status200OK, "application/json")] [ProducesResponseType(Status500InternalServerError)] public async Task>, InternalServerError>> GetAllManga () { if (await context.Mangas.Include(m => m.MangaConnectorIds).ToArrayAsync(HttpContext.RequestAborted) is not { } result) return TypedResults.InternalServerError(); return TypedResults.Ok(result.Select(m => { IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids); }).ToList()); } /// /// Returns all cached .Keys /// /// Keys/IDs /// Error during Database Operation [HttpGet("Keys")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status500InternalServerError)] public async Task, InternalServerError>> GetAllMangaKeys () { if (await context.Mangas.Select(m => m.Key).ToArrayAsync(HttpContext.RequestAborted) is not { } result) return TypedResults.InternalServerError(); return TypedResults.Ok(result); } /// /// Returns all that are being downloaded from at least one /// /// exert of . Use for more information /// Error during Database Operation [HttpGet("Downloading")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status500InternalServerError)] public async Task>, InternalServerError>> GetMangaDownloading() { if (await context.Mangas .Include(m => m.MangaConnectorIds) .Where(m => m.MangaConnectorIds.Any(id => id.UseForDownload)) .ToArrayAsync(HttpContext.RequestAborted) is not { } result) return TypedResults.InternalServerError(); return TypedResults.Ok(result.Select(m => { IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); return new MinimalManga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids); }).ToList()); } /// /// Returns all cached with /// /// Array of .Key /// /// Error during Database Operation [HttpPost("WithIDs")] [ProducesResponseType>(Status200OK, "application/json")] [ProducesResponseType(Status500InternalServerError)] public async Task>, InternalServerError>> GetMangaWithIds ([FromBody]string[] MangaIds) { if (await context.MangaIncludeAll() .Where(m => MangaIds.Contains(m.Key)) .ToArrayAsync(HttpContext.RequestAborted) is not { } result) return TypedResults.InternalServerError(); return TypedResults.Ok(result.Select(m => { IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); IEnumerable authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName)); IEnumerable tags = m.MangaTags.Select(t => t.Tag); IEnumerable links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl)); IEnumerable altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title)); return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles, m.LibraryId); }).ToList()); } /// /// Return with /// /// .Key /// /// with not found [HttpGet("{MangaId}")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound, "text/plain")] public async Task, NotFound>> GetManga (string MangaId) { if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) return TypedResults.NotFound(nameof(MangaId)); IEnumerable ids = manga.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); IEnumerable authors = manga.Authors.Select(a => new Author(a.Key, a.AuthorName)); IEnumerable tags = manga.MangaTags.Select(t => t.Tag); IEnumerable links = manga.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl)); IEnumerable altTitles = manga.AltTitles.Select(a => new AltTitle(a.Language, a.Title)); Manga result = new (manga.Key, manga.Name, manga.Description, manga.ReleaseStatus, ids, manga.IgnoreChaptersBefore, manga.Year, manga.OriginalLanguage, manga.ChapterIds, authors, tags, links, altTitles, manga.LibraryId); return TypedResults.Ok(result); } /// /// Delete with /// /// .Key /// /// with not found /// Error during Database Operation [HttpDelete("{MangaId}")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] public async Task, InternalServerError>> DeleteManga (string MangaId) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) return TypedResults.NotFound(nameof(MangaId)); context.Mangas.Remove(manga); if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) return TypedResults.InternalServerError(result.exceptionMessage); return TypedResults.Ok(); } /// /// Merge two into one. THIS IS NOT REVERSIBLE! /// /// .Key of merging data from (getting deleted) /// .Key of merging data into /// /// with or not found [HttpPatch("{MangaIdFrom}/MergeInto/{MangaIdInto}")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status404NotFound, "text/plain")] public async Task>> MergeIntoManga (string MangaIdFrom, string MangaIdInto) { if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdFrom, HttpContext.RequestAborted) is not { } from) return TypedResults.NotFound(nameof(MangaIdFrom)); if (await context.MangaIncludeAll().FirstOrDefaultAsync(m => m.Key == MangaIdInto, HttpContext.RequestAborted) is not { } into) return TypedResults.NotFound(nameof(MangaIdInto)); BaseWorker[] newJobs = into.MergeFrom(from, context); Tranga.AddWorkers(newJobs); return TypedResults.Ok(); } /// /// Returns Cover of with /// /// .Key /// Size of the cover returned ///
- ///
- ///
- /// /// JPEG Image /// Cover not loaded /// with not found /// Retry later, downloading cover [HttpGet("{MangaId}/Cover/{CoverSize?}")] [ProducesResponseType(Status200OK,"image/jpeg")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status400BadRequest)] [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status503ServiceUnavailable)] public async Task, StatusCodeHttpResult>> GetCover (string MangaId, CoverSize? CoverSize = null) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) return TypedResults.NotFound(nameof(MangaId)); string cache = CoverSize switch { MangaController.CoverSize.Small => TrangaSettings.coverImageCacheSmall, MangaController.CoverSize.Medium => TrangaSettings.coverImageCacheMedium, MangaController.CoverSize.Large => TrangaSettings.coverImageCacheLarge, _ => TrangaSettings.coverImageCacheOriginal }; if (await manga.GetCoverImage(cache, HttpContext.RequestAborted) is not { } data) { if (Tranga.GetRunningWorkers().Any(worker => worker is DownloadCoverFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId)) { Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}"); return TypedResults.StatusCode(Status503ServiceUnavailable); } return TypedResults.NoContent(); } DateTime lastModified = data.fileInfo.LastWriteTime; EntityTagHeaderValue entityTagHeaderValue = EntityTagHeaderValue.Parse($"\"{lastModified.Ticks}\""); if(HttpContext.Request.Headers.ETag.Equals(entityTagHeaderValue.Tag.Value)) return TypedResults.StatusCode(Status304NotModified); HttpContext.Response.Headers.CacheControl = "public"; return TypedResults.Bytes(data.stream.ToArray(), "image/jpeg", lastModified: new DateTimeOffset(lastModified), entityTag: entityTagHeaderValue); } public enum CoverSize { Original, Large, Medium, Small } /// /// Returns all of with /// /// .Key /// /// with not found [HttpGet("{MangaId}/Chapters")] [ProducesResponseType>(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound)] public async Task>, NotFound>> GetChapters(string MangaId) { if (await context.Mangas.Include(m => m.Chapters).ThenInclude(c => c.MangaConnectorIds).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) return TypedResults.NotFound(nameof(MangaId)); List chapters = manga.Chapters.Select(c => { IEnumerable ids = c.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded); }).ToList(); return TypedResults.Ok(chapters); } /// /// Returns all downloaded for with /// /// .Key /// /// No available chapters /// with not found. [HttpGet("{MangaId}/Chapters/Downloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound, "text/plain")] public async Task>, NoContent, NotFound>> GetChaptersDownloaded(string MangaId) { if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) return TypedResults.NotFound(nameof(MangaId)); List chapters = manga.Chapters.Where(c => c.Downloaded).Select(c => { IEnumerable ids = c.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded); }).ToList(); if (chapters.Count == 0) return TypedResults.NoContent(); return TypedResults.Ok(chapters); } /// /// Returns all not downloaded for with /// /// .Key /// /// No available chapters /// with not found. [HttpGet("{MangaId}/Chapters/NotDownloaded")] [ProducesResponseType>(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound, "text/plain")] public async Task>, NoContent, NotFound>> GetChaptersNotDownloaded(string MangaId) { if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) return TypedResults.NotFound(nameof(MangaId)); List chapters = manga.Chapters.Where(c => c.Downloaded == false).Select(c => { IEnumerable ids = c.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); return new Chapter(c.Key, c.ParentMangaId, c.VolumeNumber, c.ChapterNumber, c.Title, ids, c.Downloaded); }).ToList(); if (chapters.Count == 0) return TypedResults.NoContent(); return TypedResults.Ok(chapters); } /// /// Returns the latest of requested available on /// /// .Key /// /// No available chapters /// with not found. /// Could not retrieve the maximum chapter-number /// Retry after timeout, updating value [HttpGet("{MangaId}/Chapter/LatestAvailable")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError)] [ProducesResponseType(Status503ServiceUnavailable)] public async Task, NoContent, InternalServerError, NotFound, StatusCodeHttpResult>> GetLatestChapter(string MangaId) { if (await context.Mangas.Include(m => m.Chapters).FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) return TypedResults.NotFound(nameof(MangaId)); List chapters = manga.Chapters.ToList(); if (chapters.Count == 0) { if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed)) { Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}"); return TypedResults.StatusCode(Status503ServiceUnavailable); } else return TypedResults.NoContent(); } API.Schema.MangaContext.Chapter? max = chapters.Max(); if (max is null) return TypedResults.InternalServerError(); foreach (CollectionEntry collectionEntry in context.Entry(max).Collections) await collectionEntry.LoadAsync(HttpContext.RequestAborted); IEnumerable ids = max.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); return TypedResults.Ok(new Chapter(max.Key, max.ParentMangaId, max.VolumeNumber, max.ChapterNumber, max.Title,ids, max.Downloaded)); } /// /// Returns the latest of requested that is downloaded /// /// .Key /// /// No available chapters /// with not found. /// Could not retrieve the maximum chapter-number /// Retry after timeout, updating value [HttpGet("{MangaId}/Chapter/LatestDownloaded")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status204NoContent)] [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status412PreconditionFailed)] [ProducesResponseType(Status503ServiceUnavailable)] public async Task, NoContent, NotFound, StatusCodeHttpResult>> GetLatestChapterDownloaded(string MangaId) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) return TypedResults.NotFound(nameof(MangaId)); await context.Entry(manga).Collection(m => m.Chapters).LoadAsync(HttpContext.RequestAborted); List chapters = manga.Chapters.ToList(); if (chapters.Count == 0) { if (Tranga.GetRunningWorkers().Any(worker => worker is RetrieveMangaChaptersFromMangaconnectorWorker w && context.MangaConnectorToManga.Find(w.MangaConnectorIdId)?.ObjId == MangaId && w.State < WorkerExecutionState.Completed)) { Response.Headers.Append("Retry-After", $"{Tranga.Settings.WorkCycleTimeoutMs * 2 / 1000:D}"); return TypedResults.StatusCode(Status503ServiceUnavailable); }else return TypedResults.NoContent(); } API.Schema.MangaContext.Chapter? max = chapters.Max(); if (max is null) return TypedResults.StatusCode(Status412PreconditionFailed); IEnumerable ids = max.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); return TypedResults.Ok(new Chapter(max.Key, max.ParentMangaId, max.VolumeNumber, max.ChapterNumber, max.Title,ids, max.Downloaded)); } /// /// Configure the cut-off for /// /// .Key /// Threshold ( ChapterNumber) /// /// with not found. /// Error during Database Operation [HttpPatch("{MangaId}/IgnoreChaptersBefore")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] public async Task, InternalServerError>> IgnoreChaptersBefore(string MangaId, [FromBody]float chapterThreshold) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) return TypedResults.NotFound(nameof(MangaId)); manga.IgnoreChaptersBefore = chapterThreshold; if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) return TypedResults.InternalServerError(result.exceptionMessage); return TypedResults.Ok(); } /// /// Move to different /// /// .Key /// .Key /// Folder is going to be moved /// or not found [HttpPost("{MangaId}/ChangeLibrary/{LibraryId}")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status404NotFound, "text/plain")] public async Task>> ChangeLibrary(string MangaId, string LibraryId) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) return TypedResults.NotFound(nameof(MangaId)); if (await context.FileLibraries.FirstOrDefaultAsync(l => l.Key == LibraryId, HttpContext.RequestAborted) is not { } library) return TypedResults.NotFound(nameof(LibraryId)); if(manga.LibraryId == library.Key) return TypedResults.Ok(); MoveMangaLibraryWorker moveLibrary = new(manga, library); Tranga.AddWorkers([moveLibrary]); return TypedResults.Ok(); } /// /// (Un-)Marks as requested for Download from /// /// with /// with /// true to mark as requested, false to mark as not-requested /// /// or not found /// was not linked to , so nothing changed /// is not linked to yet. Search for on first (to create a ). /// Error during Database Operation [HttpPost("{MangaId}/SetAsDownloadFrom/{MangaConnectorName}/{IsRequested}")] [ProducesResponseType(Status200OK)] [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status412PreconditionFailed, "text/plain")] [ProducesResponseType(Status428PreconditionRequired, "text/plain")] [ProducesResponseType(Status500InternalServerError, "text/plain")] public async Task, StatusCodeHttpResult, InternalServerError>> MarkAsRequested(string MangaId, string MangaConnectorName, bool IsRequested) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } _) return TypedResults.NotFound(nameof(MangaId)); if(!Tranga.TryGetMangaConnector(MangaConnectorName, out API.MangaConnectors.MangaConnector? _)) return TypedResults.NotFound(nameof(MangaConnectorName)); if (context.MangaConnectorToManga .FirstOrDefault(id => id.MangaConnectorName == MangaConnectorName && id.ObjId == MangaId) is not { } mcId) { if(IsRequested) return TypedResults.StatusCode(Status428PreconditionRequired); else return TypedResults.StatusCode(Status412PreconditionFailed); } mcId.UseForDownload = IsRequested; if(await context.Sync(HttpContext.RequestAborted) is { success: false } result) return TypedResults.InternalServerError(result.exceptionMessage); DownloadCoverFromMangaconnectorWorker downloadCover = new(mcId); RetrieveMangaChaptersFromMangaconnectorWorker retrieveChapters = new(mcId, Tranga.Settings.DownloadLanguage); Tranga.AddWorkers([downloadCover, retrieveChapters]); return TypedResults.Ok(); } /// /// Initiate a search for on a different /// /// with /// .Name /// exert of /// with Name not found /// with Name is disabled [HttpPost("{MangaId}/SearchOn/{MangaConnectorName}")] [ProducesResponseType>(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status406NotAcceptable)] public async Task>, NotFound, StatusCodeHttpResult>> SearchOnDifferentConnector (string MangaId, string MangaConnectorName) { if (await context.Mangas.FirstOrDefaultAsync(m => m.Key == MangaId, HttpContext.RequestAborted) is not { } manga) return TypedResults.NotFound(nameof(MangaId)); return new SearchController(context).SearchManga(MangaConnectorName, manga.Name); } /// /// Returns all which where Authored by with /// /// .Key /// /// with /// /// Error during Database Operation [HttpGet("WithAuthorId/{AuthorId}")] [ProducesResponseType>(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound, "text/plain")] public async Task>, NotFound, InternalServerError>> GetMangaWithAuthorIds (string AuthorId) { if (await context.Authors.FirstOrDefaultAsync(a => a.Key == AuthorId, HttpContext.RequestAborted) is not { } _) return TypedResults.NotFound(nameof(AuthorId)); if (await context.MangaIncludeAll().Where(m => m.Authors.Any(a => a.Key == AuthorId)).ToListAsync(HttpContext.RequestAborted) is not { } result) return TypedResults.InternalServerError(); return TypedResults.Ok(result.Select(m => { IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); IEnumerable authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName)); IEnumerable tags = m.MangaTags.Select(t => t.Tag); IEnumerable links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl)); IEnumerable altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title)); return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles, m.LibraryId); }).ToList()); } /// /// Returns all with /// /// .Tag /// /// not found /// Error during Database Operation [HttpGet("WithTag/{Tag}")] [ProducesResponseType(Status200OK, "application/json")] [ProducesResponseType(Status404NotFound, "text/plain")] [ProducesResponseType(Status500InternalServerError)] public async Task>, NotFound, InternalServerError>> GetMangasWithTag (string Tag) { if (await context.Tags.FirstOrDefaultAsync(t => t.Tag == Tag, HttpContext.RequestAborted) is not { } tag) return TypedResults.NotFound(nameof(Tag)); if (await context.MangaIncludeAll().Where(m => m.MangaTags.Any(t => t.Tag.Equals(tag))) .ToListAsync(HttpContext.RequestAborted) is not { } result) return TypedResults.InternalServerError(); return TypedResults.Ok(result.Select(m => { IEnumerable ids = m.MangaConnectorIds.Select(id => new MangaConnectorId(id.Key, id.MangaConnectorName, id.ObjId, id.WebsiteUrl, id.UseForDownload)); IEnumerable authors = m.Authors.Select(a => new Author(a.Key, a.AuthorName)); IEnumerable tags = m.MangaTags.Select(t => t.Tag); IEnumerable links = m.Links.Select(l => new Link(l.Key, l.LinkProvider, l.LinkUrl)); IEnumerable altTitles = m.AltTitles.Select(a => new AltTitle(a.Language, a.Title)); return new Manga(m.Key, m.Name, m.Description, m.ReleaseStatus, ids, m.IgnoreChaptersBefore, m.Year, m.OriginalLanguage, m.ChapterIds, authors, tags, links, altTitles, m.LibraryId); }).ToList()); } }