diff --git a/Server/Bibblan.csproj b/Server/Bibblan.csproj index 3defcaf..3fa154e 100644 --- a/Server/Bibblan.csproj +++ b/Server/Bibblan.csproj @@ -14,6 +14,8 @@ 9.*-* + + diff --git a/Server/Business/Services/CalibreService.cs b/Server/Business/Services/CalibreService.cs new file mode 100644 index 0000000..672af21 --- /dev/null +++ b/Server/Business/Services/CalibreService.cs @@ -0,0 +1,44 @@ +using Bibblan.Models; +using Bibblan.ViewModels; + +namespace Bibblan.Business.Services +{ + public class CalibreService + { + CalibreContext _context; + public CalibreService(CalibreContext context) + { + _context = context; + } + + public IQueryable GetAllBooks() + { + return from b in _context.Books + join ba in _context.BooksAuthorsLink on b.Id equals ba.Book + join a in _context.Authors on ba.Author equals a.Id + join comment in _context.Comments on b.Id equals comment.Book into comments + from comment in comments.DefaultIfEmpty() + join bl in _context.BooksLanguagesLink on b.Id equals bl.Book + join l in _context.Languages on bl.LangCode equals l.Id + join bs in _context.BooksSeriesLink on b.Id equals bs.Book into book_series_link + from bs in book_series_link.DefaultIfEmpty() + join s in _context.Series on bs.Series equals s.Id into series + from s in series.DefaultIfEmpty() + orderby b.Title + select new BookVm + { + Id = b.Id, + Title = b.Title, + AuthorId = a.Id, + Author = a.Sort, + Comments = comment.Text, + Language = l.LangCode, + Path = b.Path, + HasCover = b.HasCover == "1", + Formats = (from d in _context.Data where d.Book == b.Id orderby d.Format select new DataVm { Id = d.Id, Format = d.Format, FileName = d.Name + "." + d.Format.ToLower() }).ToList(), + SeriesName = s.Name, + SeriesNumber = b.SeriesIndex + }; + } + } +} diff --git a/Server/Controllers/CalibreController.cs b/Server/Controllers/CalibreController.cs new file mode 100644 index 0000000..6a1c147 --- /dev/null +++ b/Server/Controllers/CalibreController.cs @@ -0,0 +1,23 @@ +using Bibblan.Business.Services; +using Bibblan.Models; +using Microsoft.AspNetCore.Mvc; + +namespace Bibblan.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class CalibreController : ControllerBase + { + CalibreService service; + public CalibreController(CalibreContext db) { + service = new CalibreService(db); + } + + [HttpGet("books")] + public IActionResult GetBooks() + { + var books = service.GetAllBooks().Take(100).ToList(); + return Ok(books); + } + } +} diff --git a/Server/Models/CalibreContext.cs b/Server/Models/CalibreContext.cs new file mode 100644 index 0000000..e9d07c2 --- /dev/null +++ b/Server/Models/CalibreContext.cs @@ -0,0 +1,765 @@ +using Microsoft.EntityFrameworkCore; + +namespace Bibblan.Models +{ + public partial class Authors + { + public long Id { get; set; } + public string Name { get; set; } + public string Sort { get; set; } + public string Link { get; set; } + } + + public partial class Books + { + public long Id { get; set; } + public string Title { get; set; } + public string Sort { get; set; } + public string Timestamp { get; set; } + public string Pubdate { get; set; } + public double SeriesIndex { get; set; } + public string AuthorSort { get; set; } + public string Isbn { get; set; } + public string Lccn { get; set; } + public string Path { get; set; } + public long Flags { get; set; } + public string Uuid { get; set; } + public string HasCover { get; set; } + public string LastModified { get; set; } + } + + public partial class BooksAuthorsLink + { + public long Id { get; set; } + public long Book { get; set; } + public long Author { get; set; } + } + + public partial class BooksLanguagesLink + { + public long Id { get; set; } + public long Book { get; set; } + public long LangCode { get; set; } + public long ItemOrder { get; set; } + } + + public partial class BooksPluginData + { + public long Id { get; set; } + public long Book { get; set; } + public string Name { get; set; } + public string Val { get; set; } + } + + public partial class BooksPublishersLink + { + public long Id { get; set; } + public long Book { get; set; } + public long Publisher { get; set; } + } + + public partial class BooksRatingsLink + { + public long Id { get; set; } + public long Book { get; set; } + public long Rating { get; set; } + } + + public partial class BooksSeriesLink + { + public long Id { get; set; } + public long Book { get; set; } + public long Series { get; set; } + } + + public partial class BooksTagsLink + { + public long Id { get; set; } + public long Book { get; set; } + public long Tag { get; set; } + } + + + public partial class Comments + { + public long Id { get; set; } + public long Book { get; set; } + public string Text { get; set; } + } + + public partial class ConversionOptions + { + public long Id { get; set; } + public string Format { get; set; } + public long? Book { get; set; } + public byte[] Data { get; set; } + } + + public partial class CustomColumns + { + public long Id { get; set; } + public string Label { get; set; } + public string Name { get; set; } + public string Datatype { get; set; } + public string MarkForDelete { get; set; } + public string Editable { get; set; } + public string Display { get; set; } + public string IsMultiple { get; set; } + public string Normalized { get; set; } + } + + public partial class Data + { + public long Id { get; set; } + public long Book { get; set; } + public string Format { get; set; } + public long UncompressedSize { get; set; } + public string Name { get; set; } + } + + public partial class Feeds + { + public long Id { get; set; } + public string Title { get; set; } + public string Script { get; set; } + } + + public partial class Identifiers + { + public long Id { get; set; } + public long Book { get; set; } + public string Type { get; set; } + public string Val { get; set; } + } + + public partial class Languages + { + public long Id { get; set; } + public string LangCode { get; set; } + } + + public partial class LibraryId + { + public long Id { get; set; } + public string Uuid { get; set; } + } + + public partial class MetadataDirtied + { + public long Id { get; set; } + public long Book { get; set; } + } + + public partial class Preferences + { + public long Id { get; set; } + public string Key { get; set; } + public string Val { get; set; } + } + + public partial class Publishers + { + public long Id { get; set; } + public string Name { get; set; } + public string Sort { get; set; } + } + + public partial class Ratings + { + public long Id { get; set; } + public long? Rating { get; set; } + } + + public partial class Series + { + public long Id { get; set; } + public string Name { get; set; } + public string Sort { get; set; } + } + + public partial class Tags + { + public long Id { get; set; } + public string Name { get; set; } + } + + public partial class CalibreContext : DbContext + { + public virtual DbSet Authors { get; set; } + public virtual DbSet Books { get; set; } + public virtual DbSet BooksAuthorsLink { get; set; } + public virtual DbSet BooksLanguagesLink { get; set; } + public virtual DbSet BooksPluginData { get; set; } + public virtual DbSet BooksPublishersLink { get; set; } + public virtual DbSet BooksRatingsLink { get; set; } + public virtual DbSet BooksSeriesLink { get; set; } + public virtual DbSet BooksTagsLink { get; set; } + public virtual DbSet Comments { get; set; } + public virtual DbSet ConversionOptions { get; set; } + public virtual DbSet CustomColumns { get; set; } + public virtual DbSet Data { get; set; } + public virtual DbSet Feeds { get; set; } + public virtual DbSet Identifiers { get; set; } + public virtual DbSet Languages { get; set; } + public virtual DbSet LibraryId { get; set; } + public virtual DbSet MetadataDirtied { get; set; } + public virtual DbSet Preferences { get; set; } + public virtual DbSet Publishers { get; set; } + public virtual DbSet Ratings { get; set; } + public virtual DbSet Series { get; set; } + public virtual DbSet Tags { get; set; } + + public CalibreContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.ToTable("authors"); + + entity.HasIndex(e => e.Name) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Link) + .IsRequired() + .HasColumnName("link") + .HasDefaultValueSql("\"\""); + + entity.Property(e => e.Name) + .IsRequired() + .HasColumnName("name"); + + entity.Property(e => e.Sort).HasColumnName("sort"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("books"); + + entity.HasIndex(e => e.AuthorSort) + .HasDatabaseName("authors_idx"); + + entity.HasIndex(e => e.Sort) + .HasDatabaseName("books_idx"); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.AuthorSort).HasColumnName("author_sort"); + + entity.Property(e => e.Flags) + .HasColumnName("flags") + .HasDefaultValueSql("1"); + + entity.Property(e => e.HasCover) + .HasColumnName("has_cover") + .HasColumnType("BOOL") + .HasDefaultValueSql("0"); + + entity.Property(e => e.Isbn) + .HasColumnName("isbn") + .HasDefaultValueSql("\"\""); + + entity.Property(e => e.LastModified) + .IsRequired() + .HasColumnName("last_modified") + .HasColumnType("TIMESTAMP") + .HasDefaultValueSql("\"2000-01-01 00:00:00+00:00\""); + + entity.Property(e => e.Lccn) + .HasColumnName("lccn") + .HasDefaultValueSql("\"\""); + + entity.Property(e => e.Path) + .IsRequired() + .HasColumnName("path") + .HasDefaultValueSql("\"\""); + + entity.Property(e => e.Pubdate) + .HasColumnName("pubdate") + .HasColumnType("TIMESTAMP") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + entity.Property(e => e.SeriesIndex) + .HasColumnName("series_index") + .HasDefaultValueSql("1.0"); + + entity.Property(e => e.Sort).HasColumnName("sort"); + + entity.Property(e => e.Timestamp) + .HasColumnName("timestamp") + .HasColumnType("TIMESTAMP") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + entity.Property(e => e.Title) + .IsRequired() + .HasColumnName("title") + .HasDefaultValueSql("'Unknown'"); + + entity.Property(e => e.Uuid).HasColumnName("uuid"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("books_authors_link"); + + entity.HasIndex(e => e.Author) + .HasDatabaseName("books_authors_link_aidx"); + + entity.HasIndex(e => e.Book) + .HasDatabaseName("books_authors_link_bidx"); + + entity.HasIndex(e => new { e.Book, e.Author }) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Author).HasColumnName("author"); + + entity.Property(e => e.Book).HasColumnName("book"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("books_languages_link"); + + entity.HasIndex(e => e.Book) + .HasDatabaseName("books_languages_link_bidx"); + + entity.HasIndex(e => e.LangCode) + .HasDatabaseName("books_languages_link_aidx"); + + entity.HasIndex(e => new { e.Book, e.LangCode }) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Book).HasColumnName("book"); + + entity.Property(e => e.ItemOrder) + .HasColumnName("item_order") + .HasDefaultValueSql("0"); + + entity.Property(e => e.LangCode).HasColumnName("lang_code"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("books_plugin_data"); + + entity.HasIndex(e => new { e.Book, e.Name }) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Book).HasColumnName("book"); + + entity.Property(e => e.Name) + .IsRequired() + .HasColumnName("name"); + + entity.Property(e => e.Val) + .IsRequired() + .HasColumnName("val"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("books_publishers_link"); + + entity.HasIndex(e => e.Book) + .HasDatabaseName("books_publishers_link_bidx"); + + entity.HasIndex(e => e.Publisher) + .HasDatabaseName("books_publishers_link_aidx"); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Book).HasColumnName("book"); + + entity.Property(e => e.Publisher).HasColumnName("publisher"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("books_ratings_link"); + + entity.HasIndex(e => e.Book) + .HasDatabaseName("books_ratings_link_bidx"); + + entity.HasIndex(e => e.Rating) + .HasDatabaseName("books_ratings_link_aidx"); + + entity.HasIndex(e => new { e.Book, e.Rating }) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Book).HasColumnName("book"); + + entity.Property(e => e.Rating).HasColumnName("rating"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("books_series_link"); + + entity.HasIndex(e => e.Book) + .HasDatabaseName("books_series_link_bidx"); + + entity.HasIndex(e => e.Series) + .HasDatabaseName("books_series_link_aidx"); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Book).HasColumnName("book"); + + entity.Property(e => e.Series).HasColumnName("series"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("books_tags_link"); + + entity.HasIndex(e => e.Book) + .HasDatabaseName("books_tags_link_bidx"); + + entity.HasIndex(e => e.Tag) + .HasDatabaseName("books_tags_link_aidx"); + + entity.HasIndex(e => new { e.Book, e.Tag }) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Book).HasColumnName("book"); + + entity.Property(e => e.Tag).HasColumnName("tag"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("comments"); + + entity.HasIndex(e => e.Book) + .HasDatabaseName("comments_idx"); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Book).HasColumnName("book"); + + entity.Property(e => e.Text) + .IsRequired() + .HasColumnName("text"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("conversion_options"); + + entity.HasIndex(e => e.Book) + .HasDatabaseName("conversion_options_idx_b"); + + entity.HasIndex(e => e.Format) + .HasDatabaseName("conversion_options_idx_a"); + + entity.HasIndex(e => new { e.Format, e.Book }) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Book).HasColumnName("book"); + + entity.Property(e => e.Data) + .IsRequired() + .HasColumnName("data"); + + entity.Property(e => e.Format) + .IsRequired() + .HasColumnName("format"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("custom_columns"); + + entity.HasIndex(e => e.Label) + .HasDatabaseName("custom_columns_idx"); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Datatype) + .IsRequired() + .HasColumnName("datatype"); + + entity.Property(e => e.Display) + .IsRequired() + .HasColumnName("display") + .HasDefaultValueSql("\"{}\""); + + entity.Property(e => e.Editable) + .IsRequired() + .HasColumnName("editable") + .HasColumnType("BOOL") + .HasDefaultValueSql("1"); + + entity.Property(e => e.IsMultiple) + .IsRequired() + .HasColumnName("is_multiple") + .HasColumnType("BOOL") + .HasDefaultValueSql("0"); + + entity.Property(e => e.Label) + .IsRequired() + .HasColumnName("label"); + + entity.Property(e => e.MarkForDelete) + .IsRequired() + .HasColumnName("mark_for_delete") + .HasColumnType("BOOL") + .HasDefaultValueSql("0"); + + entity.Property(e => e.Name) + .IsRequired() + .HasColumnName("name"); + + entity.Property(e => e.Normalized) + .IsRequired() + .HasColumnName("normalized") + .HasColumnType("BOOL"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("data"); + + entity.HasIndex(e => e.Book) + .HasDatabaseName("data_idx"); + + entity.HasIndex(e => e.Format) + .HasDatabaseName("formats_idx"); + + entity.HasIndex(e => new { e.Book, e.Format }) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Book).HasColumnName("book"); + + entity.Property(e => e.Format) + .IsRequired() + .HasColumnName("format"); + + entity.Property(e => e.Name) + .IsRequired() + .HasColumnName("name"); + + entity.Property(e => e.UncompressedSize).HasColumnName("uncompressed_size"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("feeds"); + + entity.HasIndex(e => e.Title) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Script) + .IsRequired() + .HasColumnName("script"); + + entity.Property(e => e.Title) + .IsRequired() + .HasColumnName("title"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("identifiers"); + + entity.HasIndex(e => new { e.Book, e.Type }) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Book).HasColumnName("book"); + + entity.Property(e => e.Type) + .IsRequired() + .HasColumnName("type") + .HasDefaultValueSql("\"isbn\""); + + entity.Property(e => e.Val) + .IsRequired() + .HasColumnName("val"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("languages"); + + entity.HasIndex(e => e.LangCode) + .HasDatabaseName("languages_idx"); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.LangCode) + .IsRequired() + .HasColumnName("lang_code"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("library_id"); + + entity.HasIndex(e => e.Uuid) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Uuid) + .IsRequired() + .HasColumnName("uuid"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("metadata_dirtied"); + + entity.HasIndex(e => e.Book) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Book).HasColumnName("book"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("preferences"); + + entity.HasIndex(e => e.Key) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Key) + .IsRequired() + .HasColumnName("key"); + + entity.Property(e => e.Val) + .IsRequired() + .HasColumnName("val"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("publishers"); + + entity.HasIndex(e => e.Name) + .HasDatabaseName("publishers_idx"); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Name) + .IsRequired() + .HasColumnName("name"); + + entity.Property(e => e.Sort).HasColumnName("sort"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("ratings"); + + entity.HasIndex(e => e.Rating) + .IsUnique(); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Rating).HasColumnName("rating"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("series"); + + entity.HasIndex(e => e.Name) + .HasDatabaseName("series_idx"); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Name) + .IsRequired() + .HasColumnName("name"); + + entity.Property(e => e.Sort).HasColumnName("sort"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("tags"); + + entity.HasIndex(e => e.Name) + .HasDatabaseName("tags_idx"); + + entity.Property(e => e.Id) + .HasColumnName("id") + .ValueGeneratedNever(); + + entity.Property(e => e.Name) + .IsRequired() + .HasColumnName("name"); + }); + } + } +} diff --git a/Server/Program.cs b/Server/Program.cs index 1b52868..2f43674 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -1,3 +1,6 @@ +using Bibblan.Models; +using Microsoft.EntityFrameworkCore; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -6,6 +9,10 @@ builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); +var db = "metadata.db"; +var connection = $"Data Source={db};Mode=ReadOnly;"; +builder.Services.AddDbContext(options => options.UseSqlite(connection)); + var app = builder.Build(); app.UseDefaultFiles(); diff --git a/Server/ViewModels/AuthorVm.cs b/Server/ViewModels/AuthorVm.cs new file mode 100644 index 0000000..082090c --- /dev/null +++ b/Server/ViewModels/AuthorVm.cs @@ -0,0 +1,9 @@ +namespace Bibblan.ViewModels +{ + public class AuthorVm + { + public long Id { get; set; } + public string Name { get; set; } + public int BookCount { get; set; } + } +} diff --git a/Server/ViewModels/BookVm.cs b/Server/ViewModels/BookVm.cs new file mode 100644 index 0000000..1dff4a9 --- /dev/null +++ b/Server/ViewModels/BookVm.cs @@ -0,0 +1,27 @@ +using Bibblan.Models; + +namespace Bibblan.ViewModels +{ + public class BookVm + { + public long Id { get; set; } + public string Title { get; set; } + public string Author { get; set; } + public long AuthorId { get; set; } + public string Comments { get; set; } + public string Language { get; set; } + public string Path { get; set; } + public List Formats { get; set; } + public bool HasCover { get; set; } + public string SeriesName { get; set; } + public double SeriesNumber { get; set; } + + public string TitleAndSeries + { + get + { + return Title + (!string.IsNullOrWhiteSpace(SeriesName) ? $" ({SeriesName + (SeriesNumber > 0 ? $" {SeriesNumber:##}" : string.Empty)})" : string.Empty); + } + } + } +} diff --git a/Server/ViewModels/DataVm.cs b/Server/ViewModels/DataVm.cs new file mode 100644 index 0000000..591c4d1 --- /dev/null +++ b/Server/ViewModels/DataVm.cs @@ -0,0 +1,9 @@ +namespace Bibblan.ViewModels +{ + public class DataVm + { + public long Id { get; set; } + public string Format { get; set; } + public string FileName { get; set; } + } +}