node build fixed
This commit is contained in:
596
seanime-2.9.10/internal/handlers/manga.go
Normal file
596
seanime-2.9.10/internal/handlers/manga.go
Normal file
@@ -0,0 +1,596 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/manga"
|
||||
manga_providers "seanime/internal/manga/providers"
|
||||
"seanime/internal/util/result"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
var (
|
||||
baseMangaCache = result.NewCache[int, *anilist.BaseManga]()
|
||||
mangaDetailsCache = result.NewCache[int, *anilist.MangaDetailsById_Media]()
|
||||
)
|
||||
|
||||
// HandleGetAnilistMangaCollection
|
||||
//
|
||||
// @summary returns the user's AniList manga collection.
|
||||
// @route /api/v1/manga/anilist/collection [GET]
|
||||
// @returns anilist.MangaCollection
|
||||
func (h *Handler) HandleGetAnilistMangaCollection(c echo.Context) error {
|
||||
|
||||
type body struct {
|
||||
BypassCache bool `json:"bypassCache"`
|
||||
}
|
||||
|
||||
var b body
|
||||
if err := c.Bind(&b); err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
collection, err := h.App.GetMangaCollection(b.BypassCache)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, collection)
|
||||
}
|
||||
|
||||
// HandleGetRawAnilistMangaCollection
|
||||
//
|
||||
// @summary returns the user's AniList manga collection.
|
||||
// @route /api/v1/manga/anilist/collection/raw [GET,POST]
|
||||
// @returns anilist.MangaCollection
|
||||
func (h *Handler) HandleGetRawAnilistMangaCollection(c echo.Context) error {
|
||||
|
||||
bypassCache := c.Request().Method == "POST"
|
||||
|
||||
// Get the user's anilist collection
|
||||
mangaCollection, err := h.App.GetRawMangaCollection(bypassCache)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, mangaCollection)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// HandleGetMangaCollection
|
||||
//
|
||||
// @summary returns the user's main manga collection.
|
||||
// @desc This is an object that contains all the user's manga entries in a structured format.
|
||||
// @route /api/v1/manga/collection [GET]
|
||||
// @returns manga.Collection
|
||||
func (h *Handler) HandleGetMangaCollection(c echo.Context) error {
|
||||
|
||||
animeCollection, err := h.App.GetMangaCollection(false)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
collection, err := manga.NewCollection(&manga.NewCollectionOptions{
|
||||
MangaCollection: animeCollection,
|
||||
Platform: h.App.AnilistPlatform,
|
||||
})
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, collection)
|
||||
}
|
||||
|
||||
// HandleGetMangaEntry
|
||||
//
|
||||
// @summary returns a manga entry for the given AniList manga id.
|
||||
// @desc This is used by the manga media entry pages to get all the data about the anime. It includes metadata and AniList list data.
|
||||
// @route /api/v1/manga/entry/{id} [GET]
|
||||
// @param id - int - true - "AniList manga media ID"
|
||||
// @returns manga.Entry
|
||||
func (h *Handler) HandleGetMangaEntry(c echo.Context) error {
|
||||
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
animeCollection, err := h.App.GetMangaCollection(false)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
entry, err := manga.NewEntry(c.Request().Context(), &manga.NewEntryOptions{
|
||||
MediaId: id,
|
||||
Logger: h.App.Logger,
|
||||
FileCacher: h.App.FileCacher,
|
||||
Platform: h.App.AnilistPlatform,
|
||||
MangaCollection: animeCollection,
|
||||
})
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
if entry != nil {
|
||||
baseMangaCache.SetT(entry.MediaId, entry.Media, 1*time.Hour)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, entry)
|
||||
}
|
||||
|
||||
// HandleGetMangaEntryDetails
|
||||
//
|
||||
// @summary returns more details about an AniList manga entry.
|
||||
// @desc This fetches more fields omitted from the base queries.
|
||||
// @route /api/v1/manga/entry/{id}/details [GET]
|
||||
// @param id - int - true - "AniList manga media ID"
|
||||
// @returns anilist.MangaDetailsById_Media
|
||||
func (h *Handler) HandleGetMangaEntryDetails(c echo.Context) error {
|
||||
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
if detailsMedia, found := mangaDetailsCache.Get(id); found {
|
||||
return h.RespondWithData(c, detailsMedia)
|
||||
}
|
||||
|
||||
details, err := h.App.AnilistPlatform.GetMangaDetails(c.Request().Context(), id)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
mangaDetailsCache.SetT(id, details, 1*time.Hour)
|
||||
|
||||
return h.RespondWithData(c, details)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// HandleGetMangaLatestChapterNumbersMap
|
||||
//
|
||||
// @summary returns the latest chapter number for all manga entries.
|
||||
// @route /api/v1/manga/latest-chapter-numbers [GET]
|
||||
// @returns map[int][]manga.MangaLatestChapterNumberItem
|
||||
func (h *Handler) HandleGetMangaLatestChapterNumbersMap(c echo.Context) error {
|
||||
ret, err := h.App.MangaRepository.GetMangaLatestChapterNumbersMap()
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, ret)
|
||||
}
|
||||
|
||||
// HandleRefetchMangaChapterContainers
|
||||
//
|
||||
// @summary refetches the chapter containers for all manga entries previously cached.
|
||||
// @route /api/v1/manga/refetch-chapter-containers [POST]
|
||||
// @returns bool
|
||||
func (h *Handler) HandleRefetchMangaChapterContainers(c echo.Context) error {
|
||||
|
||||
type body struct {
|
||||
SelectedProviderMap map[int]string `json:"selectedProviderMap"`
|
||||
}
|
||||
|
||||
var b body
|
||||
if err := c.Bind(&b); err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
mangaCollection, err := h.App.GetMangaCollection(false)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
err = h.App.MangaRepository.RefreshChapterContainers(mangaCollection, b.SelectedProviderMap)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleEmptyMangaEntryCache
|
||||
//
|
||||
// @summary empties the cache for a manga entry.
|
||||
// @desc This will empty the cache for a manga entry (chapter lists and pages), allowing the client to fetch fresh data.
|
||||
// @desc HandleGetMangaEntryChapters should be called after this to fetch the new chapter list.
|
||||
// @desc Returns 'true' if the operation was successful.
|
||||
// @route /api/v1/manga/entry/cache [DELETE]
|
||||
// @returns bool
|
||||
func (h *Handler) HandleEmptyMangaEntryCache(c echo.Context) error {
|
||||
|
||||
type body struct {
|
||||
MediaId int `json:"mediaId"`
|
||||
}
|
||||
|
||||
var b body
|
||||
if err := c.Bind(&b); err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
err := h.App.MangaRepository.EmptyMangaCache(b.MediaId)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, true)
|
||||
}
|
||||
|
||||
// HandleGetMangaEntryChapters
|
||||
//
|
||||
// @summary returns the chapters for a manga entry based on the provider.
|
||||
// @route /api/v1/manga/chapters [POST]
|
||||
// @returns manga.ChapterContainer
|
||||
func (h *Handler) HandleGetMangaEntryChapters(c echo.Context) error {
|
||||
|
||||
type body struct {
|
||||
MediaId int `json:"mediaId"`
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
|
||||
var b body
|
||||
if err := c.Bind(&b); err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
var titles []*string
|
||||
baseManga, found := baseMangaCache.Get(b.MediaId)
|
||||
if !found {
|
||||
var err error
|
||||
baseManga, err = h.App.AnilistPlatform.GetManga(c.Request().Context(), b.MediaId)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
titles = baseManga.GetAllTitles()
|
||||
baseMangaCache.SetT(b.MediaId, baseManga, 24*time.Hour)
|
||||
} else {
|
||||
titles = baseManga.GetAllTitles()
|
||||
}
|
||||
|
||||
container, err := h.App.MangaRepository.GetMangaChapterContainer(&manga.GetMangaChapterContainerOptions{
|
||||
Provider: b.Provider,
|
||||
MediaId: b.MediaId,
|
||||
Titles: titles,
|
||||
Year: baseManga.GetStartYearSafe(),
|
||||
})
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, container)
|
||||
}
|
||||
|
||||
// HandleGetMangaEntryPages
|
||||
//
|
||||
// @summary returns the pages for a manga entry based on the provider and chapter id.
|
||||
// @desc This will return the pages for a manga chapter.
|
||||
// @desc If the app is offline and the chapter is not downloaded, it will return an error.
|
||||
// @desc If the app is online and the chapter is not downloaded, it will return the pages from the provider.
|
||||
// @desc If the chapter is downloaded, it will return the appropriate struct.
|
||||
// @desc If 'double page' is requested, it will fetch image sizes and include the dimensions in the response.
|
||||
// @route /api/v1/manga/pages [POST]
|
||||
// @returns manga.PageContainer
|
||||
func (h *Handler) HandleGetMangaEntryPages(c echo.Context) error {
|
||||
|
||||
type body struct {
|
||||
MediaId int `json:"mediaId"`
|
||||
Provider string `json:"provider"`
|
||||
ChapterId string `json:"chapterId"`
|
||||
DoublePage bool `json:"doublePage"`
|
||||
}
|
||||
|
||||
var b body
|
||||
if err := c.Bind(&b); err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
container, err := h.App.MangaRepository.GetMangaPageContainer(b.Provider, b.MediaId, b.ChapterId, b.DoublePage, h.App.IsOffline())
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, container)
|
||||
}
|
||||
|
||||
// HandleGetMangaEntryDownloadedChapters
|
||||
//
|
||||
// @summary returns all download chapters for a manga entry,
|
||||
// @route /api/v1/manga/downloaded-chapters/{id} [GET]
|
||||
// @param id - int - true - "AniList manga media ID"
|
||||
// @returns []manga.ChapterContainer
|
||||
func (h *Handler) HandleGetMangaEntryDownloadedChapters(c echo.Context) error {
|
||||
|
||||
mId, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
mangaCollection, err := h.App.GetMangaCollection(false)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
container, err := h.App.MangaRepository.GetDownloadedMangaChapterContainers(mId, mangaCollection)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, container)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
var (
|
||||
anilistListMangaCache = result.NewCache[string, *anilist.ListManga]()
|
||||
)
|
||||
|
||||
// HandleAnilistListManga
|
||||
//
|
||||
// @summary returns a list of manga based on the search parameters.
|
||||
// @desc This is used by "Advanced Search" and search function.
|
||||
// @route /api/v1/manga/anilist/list [POST]
|
||||
// @returns anilist.ListManga
|
||||
func (h *Handler) HandleAnilistListManga(c echo.Context) error {
|
||||
|
||||
type body struct {
|
||||
Page *int `json:"page,omitempty"`
|
||||
Search *string `json:"search,omitempty"`
|
||||
PerPage *int `json:"perPage,omitempty"`
|
||||
Sort []*anilist.MediaSort `json:"sort,omitempty"`
|
||||
Status []*anilist.MediaStatus `json:"status,omitempty"`
|
||||
Genres []*string `json:"genres,omitempty"`
|
||||
AverageScoreGreater *int `json:"averageScore_greater,omitempty"`
|
||||
Year *int `json:"year,omitempty"`
|
||||
CountryOfOrigin *string `json:"countryOfOrigin,omitempty"`
|
||||
IsAdult *bool `json:"isAdult,omitempty"`
|
||||
Format *anilist.MediaFormat `json:"format,omitempty"`
|
||||
}
|
||||
|
||||
p := new(body)
|
||||
if err := c.Bind(p); err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
if p.Page == nil || p.PerPage == nil {
|
||||
*p.Page = 1
|
||||
*p.PerPage = 20
|
||||
}
|
||||
|
||||
isAdult := false
|
||||
if p.IsAdult != nil {
|
||||
isAdult = *p.IsAdult && h.App.Settings.GetAnilist().EnableAdultContent
|
||||
}
|
||||
|
||||
cacheKey := anilist.ListMangaCacheKey(
|
||||
p.Page,
|
||||
p.Search,
|
||||
p.PerPage,
|
||||
p.Sort,
|
||||
p.Status,
|
||||
p.Genres,
|
||||
p.AverageScoreGreater,
|
||||
nil,
|
||||
p.Year,
|
||||
p.Format,
|
||||
p.CountryOfOrigin,
|
||||
&isAdult,
|
||||
)
|
||||
|
||||
cached, ok := anilistListMangaCache.Get(cacheKey)
|
||||
if ok {
|
||||
return h.RespondWithData(c, cached)
|
||||
}
|
||||
|
||||
ret, err := anilist.ListMangaM(
|
||||
p.Page,
|
||||
p.Search,
|
||||
p.PerPage,
|
||||
p.Sort,
|
||||
p.Status,
|
||||
p.Genres,
|
||||
p.AverageScoreGreater,
|
||||
p.Year,
|
||||
p.Format,
|
||||
p.CountryOfOrigin,
|
||||
&isAdult,
|
||||
h.App.Logger,
|
||||
h.App.GetUserAnilistToken(),
|
||||
)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
if ret != nil {
|
||||
anilistListMangaCache.SetT(cacheKey, ret, time.Minute*10)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, ret)
|
||||
}
|
||||
|
||||
// HandleUpdateMangaProgress
|
||||
//
|
||||
// @summary updates the progress of a manga entry.
|
||||
// @desc Note: MyAnimeList is not supported
|
||||
// @route /api/v1/manga/update-progress [POST]
|
||||
// @returns bool
|
||||
func (h *Handler) HandleUpdateMangaProgress(c echo.Context) error {
|
||||
|
||||
type body struct {
|
||||
MediaId int `json:"mediaId"`
|
||||
MalId int `json:"malId,omitempty"`
|
||||
ChapterNumber int `json:"chapterNumber"`
|
||||
TotalChapters int `json:"totalChapters"`
|
||||
}
|
||||
|
||||
b := new(body)
|
||||
if err := c.Bind(b); err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
// Update the progress on AniList
|
||||
err := h.App.AnilistPlatform.UpdateEntryProgress(
|
||||
c.Request().Context(),
|
||||
b.MediaId,
|
||||
b.ChapterNumber,
|
||||
&b.TotalChapters,
|
||||
)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
_, _ = h.App.RefreshMangaCollection() // Refresh the AniList collection
|
||||
|
||||
return h.RespondWithData(c, true)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// HandleMangaManualSearch
|
||||
//
|
||||
// @summary returns search results for a manual search.
|
||||
// @desc Returns search results for a manual search.
|
||||
// @route /api/v1/manga/search [POST]
|
||||
// @returns []hibikemanga.SearchResult
|
||||
func (h *Handler) HandleMangaManualSearch(c echo.Context) error {
|
||||
|
||||
type body struct {
|
||||
Provider string `json:"provider"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
var b body
|
||||
if err := c.Bind(&b); err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
ret, err := h.App.MangaRepository.ManualSearch(b.Provider, b.Query)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, ret)
|
||||
}
|
||||
|
||||
// HandleMangaManualMapping
|
||||
//
|
||||
// @summary manually maps a manga entry to a manga ID from the provider.
|
||||
// @desc This is used to manually map a manga entry to a manga ID from the provider.
|
||||
// @desc The client should re-fetch the chapter container after this.
|
||||
// @route /api/v1/manga/manual-mapping [POST]
|
||||
// @returns bool
|
||||
func (h *Handler) HandleMangaManualMapping(c echo.Context) error {
|
||||
|
||||
type body struct {
|
||||
Provider string `json:"provider"`
|
||||
MediaId int `json:"mediaId"`
|
||||
MangaId string `json:"mangaId"`
|
||||
}
|
||||
|
||||
var b body
|
||||
if err := c.Bind(&b); err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
err := h.App.MangaRepository.ManualMapping(b.Provider, b.MediaId, b.MangaId)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, true)
|
||||
}
|
||||
|
||||
// HandleGetMangaMapping
|
||||
//
|
||||
// @summary returns the mapping for a manga entry.
|
||||
// @desc This is used to get the mapping for a manga entry.
|
||||
// @desc An empty string is returned if there's no manual mapping. If there is, the manga ID will be returned.
|
||||
// @route /api/v1/manga/get-mapping [POST]
|
||||
// @returns manga.MappingResponse
|
||||
func (h *Handler) HandleGetMangaMapping(c echo.Context) error {
|
||||
|
||||
type body struct {
|
||||
Provider string `json:"provider"`
|
||||
MediaId int `json:"mediaId"`
|
||||
}
|
||||
|
||||
var b body
|
||||
if err := c.Bind(&b); err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
mapping := h.App.MangaRepository.GetMapping(b.Provider, b.MediaId)
|
||||
return h.RespondWithData(c, mapping)
|
||||
}
|
||||
|
||||
// HandleRemoveMangaMapping
|
||||
//
|
||||
// @summary removes the mapping for a manga entry.
|
||||
// @desc This is used to remove the mapping for a manga entry.
|
||||
// @desc The client should re-fetch the chapter container after this.
|
||||
// @route /api/v1/manga/remove-mapping [POST]
|
||||
// @returns bool
|
||||
func (h *Handler) HandleRemoveMangaMapping(c echo.Context) error {
|
||||
|
||||
type body struct {
|
||||
Provider string `json:"provider"`
|
||||
MediaId int `json:"mediaId"`
|
||||
}
|
||||
|
||||
var b body
|
||||
if err := c.Bind(&b); err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
err := h.App.MangaRepository.RemoveMapping(b.Provider, b.MediaId)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return h.RespondWithData(c, true)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// HandleGetLocalMangaPage
|
||||
//
|
||||
// @summary returns a local manga page.
|
||||
// @route /api/v1/manga/local-page/{path} [GET]
|
||||
// @returns manga.PageContainer
|
||||
func (h *Handler) HandleGetLocalMangaPage(c echo.Context) error {
|
||||
|
||||
path := c.Param("path")
|
||||
path, err := url.PathUnescape(path)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
path = strings.TrimPrefix(path, manga_providers.LocalServePath)
|
||||
|
||||
providerExtension, ok := extension.GetExtension[extension.MangaProviderExtension](h.App.ExtensionRepository.GetExtensionBank(), manga_providers.LocalProvider)
|
||||
if !ok {
|
||||
return h.RespondWithError(c, errors.New("manga: Local provider not found"))
|
||||
}
|
||||
|
||||
localProvider, ok := providerExtension.GetProvider().(*manga_providers.Local)
|
||||
if !ok {
|
||||
return h.RespondWithError(c, errors.New("manga: Local provider not found"))
|
||||
}
|
||||
|
||||
reader, err := localProvider.ReadPage(path)
|
||||
if err != nil {
|
||||
return h.RespondWithError(c, err)
|
||||
}
|
||||
|
||||
return c.Stream(http.StatusOK, "image/jpeg", reader)
|
||||
}
|
||||
Reference in New Issue
Block a user