OPDS Support (#526)

* Added some basic OPDS implementation

* Fixed an issue with feed href

* More changes

* Added library routes and moved user code to a method so we can hack in fixed code without authentication

* Images now load on the OPDS reusing our existing Image infrastructure.

* Added the ability to download and moved some download code to a dedicated service

* Download is working, pagination is implemented.

* Refactored libraries to use pagination

* Laid foundation for OpenSearch implementation

* Fixed up some serialization issues and some old code that wasn't referencing helper methods

* Ensure chapters are sorted when we send them over OPDS

* OpenSearch implemented

* Removed any support for OPDS-PS due to lack of apps supporting it.

* Don't distribute development.json nor stats directory on build.

* Implemented In Progress feed as well.

* Ability to enable OPDS for server. OPDS now accepts initial call as POST in case app uses username/password.

* UI now properly renders state for OPDS enablement. Added Collections routes.

* Fixed pagination startIndex on OPDS feeds when there is less than 1 page.

* Chunky Reader now works. It only accepts UTF-8 encodings

* More Chunky fixes

* More chunky changes, such a fussy client.

* Implemented the ability to have a custom api key assigned to a user and use that api key as your authentication token against OPDS routing.

* Implemented the ability to reset your API Key

* Fixed favicon not being sent back correctly

* Fixed an issue where images wouldn't send on OPDS feed.

* Implemented Page streaming and fixed a pagination bug

* Hooked in the ability to save progress in Kavita when Page Streaming
This commit is contained in:
Joseph Milazzo 2021-08-27 10:19:25 -07:00 committed by GitHub
parent 2a63e5e9e2
commit 6069d93c38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 2409 additions and 116 deletions

View file

@ -4,6 +4,9 @@ namespace API.DTOs.Filtering
{
public class FilterDto
{
/// <summary>
/// Pass null if you want all formats
/// </summary>
public MangaFormat? MangaFormat { get; init; } = null;
}

12
API/DTOs/OPDS/Author.cs Normal file
View file

@ -0,0 +1,12 @@
using System.Xml.Serialization;
namespace API.DTOs.OPDS
{
public class Author
{
[XmlElement("name")]
public string Name { get; set; }
[XmlElement("uri")]
public string Uri { get; set; }
}
}

62
API/DTOs/OPDS/Feed.cs Normal file
View file

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
namespace API.DTOs.OPDS
{
/// <summary>
///
/// </summary>
[XmlRoot("feed", Namespace = "http://www.w3.org/2005/Atom")]
public class Feed
{
[XmlElement("updated")]
public string Updated { get; init; } = DateTime.UtcNow.ToString("s");
[XmlElement("id")]
public string Id { get; set; }
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("icon")]
public string Icon { get; set; } = "/favicon.ico";
[XmlElement("author")]
public Author Author { get; set; } = new Author()
{
Name = "Kavita",
Uri = "https://kavitareader.com"
};
[XmlElement("totalResults", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public int? Total { get; set; } = null;
[XmlElement("itemsPerPage", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public int? ItemsPerPage { get; set; } = null;
[XmlElement("startIndex", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public int? StartIndex { get; set; } = null;
[XmlElement("link")]
public List<FeedLink> Links { get; set; } = new List<FeedLink>() ;
[XmlElement("entry")]
public List<FeedEntry> Entries { get; set; } = new List<FeedEntry>();
public bool ShouldSerializeTotal()
{
return Total.HasValue;
}
public bool ShouldSerializeItemsPerPage()
{
return ItemsPerPage.HasValue;
}
public bool ShouldSerializeStartIndex()
{
return StartIndex.HasValue;
}
}
}

View file

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Xml.Serialization;
namespace API.DTOs.OPDS
{
public class FeedEntry
{
[XmlElement("updated")]
public string Updated { get; init; } = DateTime.UtcNow.ToString("s");
[XmlElement("id")]
public string Id { get; set; }
[XmlElement("title")]
public string Title { get; set; }
[XmlElement("summary")]
public string Summary { get; set; }
/// <summary>
/// Represents Size of the Entry
/// Tag: , ElementName = "dcterms:extent"
/// <example>2 MB</example>
/// </summary>
[XmlElement("extent", Namespace = "http://purl.org/dc/terms/")]
public string Extent { get; set; }
/// <summary>
/// Format of the file
/// https://dublincore.org/specifications/dublin-core/dcmi-terms/
/// </summary>
[XmlElement("format", Namespace = "http://purl.org/dc/terms/format")]
public string Format { get; set; }
[XmlElement("language", Namespace = "http://purl.org/dc/terms/")]
public string Language { get; set; }
[XmlElement("content")]
public FeedEntryContent Content { get; set; }
[XmlElement("link")]
public List<FeedLink> Links = new List<FeedLink>();
// [XmlElement("author")]
// public List<FeedAuthor> Authors = new List<FeedAuthor>();
// [XmlElement("category")]
// public List<FeedCategory> Categories = new List<FeedCategory>();
}
}

View file

@ -0,0 +1,12 @@
using System.Xml.Serialization;
namespace API.DTOs.OPDS
{
public class FeedEntryContent
{
[XmlAttribute("type")]
public string Type = "text";
[XmlText]
public string Text;
}
}

33
API/DTOs/OPDS/FeedLink.cs Normal file
View file

@ -0,0 +1,33 @@
using System.Xml.Serialization;
namespace API.DTOs.OPDS
{
public class FeedLink
{
/// <summary>
/// Relation on the Link
/// </summary>
[XmlAttribute("rel")]
public string Rel { get; set; }
/// <summary>
/// Should be any of the types here <see cref="FeedLinkType"/>
/// </summary>
[XmlAttribute("type")]
public string Type { get; set; }
[XmlAttribute("href")]
public string Href { get; set; }
[XmlAttribute("title")]
public string Title { get; set; }
[XmlAttribute("count", Namespace = "http://vaemendis.net/opds-pse/ns")]
public int TotalPages { get; set; } = 0;
public bool ShouldSerializeTotalPages()
{
return TotalPages > 0;
}
}
}

View file

@ -0,0 +1,24 @@
namespace API.DTOs.OPDS
{
public static class FeedLinkRelation
{
public const string Debug = "debug";
public const string Search = "search";
public const string Self = "self";
public const string Start = "start";
public const string Next = "next";
public const string Prev = "prev";
public const string Alternate = "alternate";
public const string SubSection = "subsection";
public const string Related = "related";
public const string Image = "http://opds-spec.org/image";
public const string Thumbnail = "http://opds-spec.org/image/thumbnail";
/// <summary>
/// This will allow for a download to occur
/// </summary>
public const string Acquisition = "http://opds-spec.org/acquisition/open-access";
#pragma warning disable S1075
public const string Stream = "http://vaemendis.net/opds-pse/stream";
#pragma warning restore S1075
}
}

View file

@ -0,0 +1,11 @@
namespace API.DTOs.OPDS
{
public static class FeedLinkType
{
public const string Atom = "application/atom+xml";
public const string AtomSearch = "application/opensearchdescription+xml";
public const string AtomNavigation = "application/atom+xml;profile=opds-catalog;kind=navigation";
public const string AtomAcquisition = "application/atom+xml;profile=opds-catalog;kind=acquisition";
public const string Image = "image/jpeg";
}
}

View file

@ -0,0 +1,42 @@
using System.Xml.Serialization;
namespace API.DTOs.OPDS
{
[XmlRoot("OpenSearchDescription", Namespace = "http://a9.com/-/spec/opensearch/1.1/")]
public class OpenSearchDescription
{
/// <summary>
/// Contains a brief human-readable title that identifies this search engine.
/// </summary>
public string ShortName { get; set; }
/// <summary>
/// Contains an extended human-readable title that identifies this search engine.
/// </summary>
public string LongName { get; set; }
/// <summary>
/// Contains a human-readable text description of the search engine.
/// </summary>
public string Description { get; set; }
/// <summary>
/// https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md#the-url-element
/// </summary>
public SearchLink Url { get; set; }
/// <summary>
/// Contains a set of words that are used as keywords to identify and categorize this search content.
/// Tags must be a single word and are delimited by the space character (' ').
/// </summary>
public string Tags { get; set; }
/// <summary>
/// Contains a URL that identifies the location of an image that can be used in association with this search content.
/// <example><Image height="64" width="64" type="image/png">http://example.com/websearch.png</Image></example>
/// </summary>
public string Image { get; set; }
public string InputEncoding { get; set; } = "UTF-8";
public string OutputEncoding { get; set; } = "UTF-8";
/// <summary>
/// Contains the human-readable name or identifier of the creator or maintainer of the description document.
/// </summary>
public string Developer { get; set; } = "kavitareader.com";
}
}

View file

@ -0,0 +1,16 @@
using System.Xml.Serialization;
namespace API.DTOs.OPDS
{
public class SearchLink
{
[XmlAttribute("type")]
public string Type { get; set; }
[XmlAttribute("rel")]
public string Rel { get; set; } = "results";
[XmlAttribute("template")]
public string Template { get; set; }
}
}

View file

@ -4,9 +4,22 @@
{
public string CacheDirectory { get; set; }
public string TaskScan { get; set; }
/// <summary>
/// Logging level for server. Managed in appsettings.json.
/// </summary>
public string LoggingLevel { get; set; }
public string TaskBackup { get; set; }
/// <summary>
/// Port the server listens on. Managed in appsettings.json.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Allows anonymous information to be collected and sent to KavitaStats
/// </summary>
public bool AllowStatCollection { get; set; }
/// <summary>
/// Enables OPDS connections to be made to the server.
/// </summary>
public bool EnableOpds { get; set; }
}
}
}

View file

@ -5,6 +5,7 @@ namespace API.DTOs
{
public string Username { get; init; }
public string Token { get; init; }
public string ApiKey { get; init; }
public UserPreferencesDto Preferences { get; set; }
}
}
}