node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View 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)
}