mirror of
https://github.com/5rahim/seanime
synced 2026-04-18 22:24:55 +02:00
update torrent search, onlinestream search
This commit is contained in:
@@ -41002,8 +41002,8 @@
|
||||
"name": "ParsedData",
|
||||
"jsonName": "parsedData",
|
||||
"goType": "habari.Metadata",
|
||||
"typescriptType": "Metadata",
|
||||
"usedTypescriptType": "Metadata",
|
||||
"typescriptType": "Habari_Metadata",
|
||||
"usedTypescriptType": "Habari_Metadata",
|
||||
"usedStructName": "habari.Metadata",
|
||||
"required": false,
|
||||
"public": true,
|
||||
@@ -66224,6 +66224,36 @@
|
||||
],
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/torrents/torrent/search.go",
|
||||
"filename": "search.go",
|
||||
"name": "TorrentMetadata",
|
||||
"formattedName": "Torrent_TorrentMetadata",
|
||||
"package": "torrent",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Distance",
|
||||
"jsonName": "distance",
|
||||
"goType": "int",
|
||||
"typescriptType": "number",
|
||||
"required": true,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Metadata",
|
||||
"jsonName": "metadata",
|
||||
"goType": "habari.Metadata",
|
||||
"typescriptType": "Habari_Metadata",
|
||||
"usedTypescriptType": "Habari_Metadata",
|
||||
"usedStructName": "habari.Metadata",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
}
|
||||
],
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/torrents/torrent/search.go",
|
||||
"filename": "search.go",
|
||||
@@ -66257,6 +66287,19 @@
|
||||
" TorrentPreview for each torrent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TorrentMetadata",
|
||||
"jsonName": "torrentMetadata",
|
||||
"goType": "map[string]TorrentMetadata",
|
||||
"typescriptType": "Record\u003cstring, Torrent_TorrentMetadata\u003e",
|
||||
"usedTypescriptType": "Torrent_TorrentMetadata",
|
||||
"usedStructName": "torrent.TorrentMetadata",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": [
|
||||
" Torrent metadata"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DebridInstantAvailability",
|
||||
"jsonName": "debridInstantAvailability",
|
||||
@@ -66269,6 +66312,19 @@
|
||||
"comments": [
|
||||
" Debrid instant availability"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AnimeMetadata",
|
||||
"jsonName": "animeMetadata",
|
||||
"goType": "metadata.AnimeMetadata",
|
||||
"typescriptType": "Metadata_AnimeMetadata",
|
||||
"usedTypescriptType": "Metadata_AnimeMetadata",
|
||||
"usedStructName": "metadata.AnimeMetadata",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": [
|
||||
" AniZip media"
|
||||
]
|
||||
}
|
||||
],
|
||||
"comments": []
|
||||
@@ -69592,5 +69648,231 @@
|
||||
}
|
||||
],
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"filepath": "../internal/vendor_habari/vendor_habari.go",
|
||||
"filename": "vendor_habari.go",
|
||||
"name": "Metadata",
|
||||
"formattedName": "Habari_Metadata",
|
||||
"package": "vendor_habari",
|
||||
"fields": [
|
||||
{
|
||||
"name": "SeasonNumber",
|
||||
"jsonName": "season_number",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "PartNumber",
|
||||
"jsonName": "part_number",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Title",
|
||||
"jsonName": "title",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "FormattedTitle",
|
||||
"jsonName": "formatted_title",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "AnimeType",
|
||||
"jsonName": "anime_type",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Year",
|
||||
"jsonName": "year",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "AudioTerm",
|
||||
"jsonName": "audio_term",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "DeviceCompatibility",
|
||||
"jsonName": "device_compatibility",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "EpisodeNumber",
|
||||
"jsonName": "episode_number",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "OtherEpisodeNumber",
|
||||
"jsonName": "other_episode_number",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "EpisodeNumberAlt",
|
||||
"jsonName": "episode_number_alt",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "EpisodeTitle",
|
||||
"jsonName": "episode_title",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "FileChecksum",
|
||||
"jsonName": "file_checksum",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "FileExtension",
|
||||
"jsonName": "file_extension",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "FileName",
|
||||
"jsonName": "file_name",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Language",
|
||||
"jsonName": "language",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "ReleaseGroup",
|
||||
"jsonName": "release_group",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "ReleaseInformation",
|
||||
"jsonName": "release_information",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "ReleaseVersion",
|
||||
"jsonName": "release_version",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Source",
|
||||
"jsonName": "source",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "Subtitles",
|
||||
"jsonName": "subtitles",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "VideoResolution",
|
||||
"jsonName": "video_resolution",
|
||||
"goType": "string",
|
||||
"typescriptType": "string",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "VideoTerm",
|
||||
"jsonName": "video_term",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
},
|
||||
{
|
||||
"name": "VolumeNumber",
|
||||
"jsonName": "volume_number",
|
||||
"goType": "[]string",
|
||||
"typescriptType": "Array\u003cstring\u003e",
|
||||
"required": false,
|
||||
"public": true,
|
||||
"comments": []
|
||||
}
|
||||
],
|
||||
"comments": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -111,6 +111,8 @@ var typePrefixesByPackage = map[string]string{
|
||||
"debrid": "Debrid_",
|
||||
"debrid_client": "DebridClient_",
|
||||
"report": "Report_",
|
||||
"habari": "Habari_",
|
||||
"vendor_habari": "Habari_",
|
||||
}
|
||||
|
||||
func getTypePrefix(packageName string) string {
|
||||
|
||||
@@ -22,6 +22,7 @@ var additionalStructNames = []string{
|
||||
"torrentstream.TorrentStatus",
|
||||
"debrid_client.StreamState",
|
||||
"extension_repo.TrayPluginExtensionItem",
|
||||
"vendor_habari.Metadata",
|
||||
}
|
||||
|
||||
// GenerateTypescriptFile generates a Typescript file containing the types for the API routes parameters and responses based on the Docs struct.
|
||||
|
||||
2
go.mod
2
go.mod
@@ -4,7 +4,7 @@ go 1.24.1
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.11.0
|
||||
github.com/5rahim/habari v0.1.4
|
||||
github.com/5rahim/habari v0.1.5
|
||||
github.com/Masterminds/semver/v3 v3.3.1
|
||||
github.com/Microsoft/go-winio v0.6.2
|
||||
github.com/PuerkitoBio/goquery v1.10.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -6,8 +6,8 @@ filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmG
|
||||
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/5rahim/habari v0.1.4 h1:KVSK6wuAaSX9UN9dcmfyjIHwrBsAk5lShumRNH8RHHs=
|
||||
github.com/5rahim/habari v0.1.4/go.mod h1:0nBj4/5OxTAoIICP4P3+/YJGNf8L7w+gnU1ivj7nFJA=
|
||||
github.com/5rahim/habari v0.1.5 h1:cYqPtHooXV5yNafogZTWf6OWn+sh0CkB5V5IVV/Q7Ww=
|
||||
github.com/5rahim/habari v0.1.5/go.mod h1:0nBj4/5OxTAoIICP4P3+/YJGNf8L7w+gnU1ivj7nFJA=
|
||||
github.com/99designs/gqlgen v0.17.54 h1:AsF49k/7RJlwA00RQYsYN0T8cQuaosnV/7G1dHC3Uh8=
|
||||
github.com/99designs/gqlgen v0.17.54/go.mod h1:77/+pVe6zlTsz++oUg2m8VLgzdUPHxjoAG3BxI5y8Rc=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
|
||||
@@ -12,7 +12,14 @@ func (m *BaseAnime) GetTitleSafe() string {
|
||||
if m.GetTitle().GetRomaji() != nil {
|
||||
return *m.GetTitle().GetRomaji()
|
||||
}
|
||||
return "N/A"
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetEnglishTitleSafe() string {
|
||||
if m.GetTitle().GetEnglish() != nil {
|
||||
return *m.GetTitle().GetEnglish()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetRomajiTitleSafe() string {
|
||||
@@ -22,7 +29,7 @@ func (m *BaseAnime) GetRomajiTitleSafe() string {
|
||||
if m.GetTitle().GetEnglish() != nil {
|
||||
return *m.GetTitle().GetEnglish()
|
||||
}
|
||||
return "N/A"
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetPreferredTitle() string {
|
||||
|
||||
@@ -13,6 +13,9 @@ type (
|
||||
}
|
||||
|
||||
SearchOptions struct {
|
||||
// The media object provided by Seanime.
|
||||
Media Media `json:"media"`
|
||||
// The search query.
|
||||
Query string `json:"query"`
|
||||
// Whether to search for subbed or dubbed anime.
|
||||
Dub bool `json:"dub"`
|
||||
@@ -21,6 +24,40 @@ type (
|
||||
Year int `json:"year"`
|
||||
}
|
||||
|
||||
Media struct {
|
||||
// AniList ID of the media.
|
||||
ID int `json:"id"`
|
||||
// MyAnimeList ID of the media.
|
||||
IDMal *int `json:"idMal,omitempty"`
|
||||
// e.g. "FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"
|
||||
// This will be set to "NOT_YET_RELEASED" if the status is unknown.
|
||||
Status string `json:"status,omitempty"`
|
||||
// e.g. "TV", "TV_SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
|
||||
// This will be set to "TV" if the format is unknown.
|
||||
Format string `json:"format,omitempty"`
|
||||
// e.g. "Attack on Titan"
|
||||
// This will be undefined if the english title is unknown.
|
||||
EnglishTitle *string `json:"englishTitle,omitempty"`
|
||||
// e.g. "Shingeki no Kyojin"
|
||||
RomajiTitle string `json:"romajiTitle,omitempty"`
|
||||
// TotalEpisodes is total number of episodes of the media.
|
||||
// This will be -1 if the total number of episodes is unknown / not applicable.
|
||||
EpisodeCount int `json:"episodeCount,omitempty"`
|
||||
// All alternative titles of the media.
|
||||
Synonyms []string `json:"synonyms"`
|
||||
// Whether the media is NSFW.
|
||||
IsAdult bool `json:"isAdult"`
|
||||
// Start date of the media.
|
||||
// This will be undefined if it has no start date.
|
||||
StartDate *FuzzyDate `json:"startDate,omitempty"`
|
||||
}
|
||||
|
||||
FuzzyDate struct {
|
||||
Year int `json:"year"`
|
||||
Month *int `json:"month"`
|
||||
Day *int `json:"day"`
|
||||
}
|
||||
|
||||
Settings struct {
|
||||
EpisodeServers []string `json:"episodeServers"`
|
||||
SupportsDub bool `json:"supportsDub"`
|
||||
|
||||
@@ -36,7 +36,28 @@ declare type VideoSubtitle = {
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
declare interface Media {
|
||||
id: number
|
||||
idMal?: number
|
||||
status?: string
|
||||
format?: string
|
||||
englishTitle?: string
|
||||
romajiTitle?: string
|
||||
episodeCount?: number
|
||||
absoluteSeasonOffset?: number
|
||||
synonyms: string[]
|
||||
isAdult: boolean
|
||||
startDate?: FuzzyDate
|
||||
}
|
||||
|
||||
declare interface FuzzyDate {
|
||||
year: number
|
||||
month?: number
|
||||
day?: number
|
||||
}
|
||||
|
||||
declare type SearchOptions = {
|
||||
media: Media
|
||||
query: string
|
||||
dub: boolean
|
||||
year?: number
|
||||
|
||||
@@ -2721,7 +2721,7 @@ declare namespace $app {
|
||||
* - Filepath: internal/library/autodownloader/autodownloader_torrent.go
|
||||
*/
|
||||
interface AutoDownloader_NormalizedTorrent {
|
||||
parsedData?: $habari.Metadata;
|
||||
parsedData?: Habari_Metadata;
|
||||
/**
|
||||
* Access using GetMagnet()
|
||||
*/
|
||||
|
||||
@@ -162,7 +162,7 @@ func (r *Repository) GetMediaEpisodes(provider string, media *anilist.BaseAnime,
|
||||
|
||||
// Fetch the episode list from the provider
|
||||
// "from" and "to" are set to 0 in order not to fetch episode servers
|
||||
ec, err := r.getEpisodeContainer(provider, mId, media.GetAllTitles(), 0, 0, dubbed, media.GetStartYearSafe())
|
||||
ec, err := r.getEpisodeContainer(provider, media, 0, 0, dubbed, media.GetStartYearSafe())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -240,7 +240,7 @@ func (r *Repository) GetEpisodeSources(provider string, mId int, number int, dub
|
||||
// | Episode servers |
|
||||
// +---------------------+
|
||||
|
||||
ec, err := r.getEpisodeContainer(provider, mId, media.GetAllTitles(), number, number, dubbed, year)
|
||||
ec, err := r.getEpisodeContainer(provider, media, number, number, dubbed, year)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ package onlinestream
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/extension"
|
||||
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
|
||||
"seanime/internal/onlinestream/providers"
|
||||
onlinestream_providers "seanime/internal/onlinestream/providers"
|
||||
"seanime/internal/util/comparison"
|
||||
"strings"
|
||||
)
|
||||
@@ -42,11 +43,11 @@ type (
|
||||
// - This function can be used to only get the episode details by setting 'from' and 'to' to 0.
|
||||
//
|
||||
// Since the episode details are cached, we can request episode servers multiple times without fetching the episode details again.
|
||||
func (r *Repository) getEpisodeContainer(provider string, mId int, titles []*string, from int, to int, dubbed bool, year int) (*episodeContainer, error) {
|
||||
func (r *Repository) getEpisodeContainer(provider string, media *anilist.BaseAnime, from int, to int, dubbed bool, year int) (*episodeContainer, error) {
|
||||
|
||||
r.logger.Debug().
|
||||
Str("provider", provider).
|
||||
Int("mediaId", mId).
|
||||
Int("mediaId", media.ID).
|
||||
Int("from", from).
|
||||
Int("to", to).
|
||||
Bool("dubbed", dubbed).
|
||||
@@ -55,7 +56,7 @@ func (r *Repository) getEpisodeContainer(provider string, mId int, titles []*str
|
||||
// Key identifying the provider episode list in the file cache.
|
||||
// It includes "dubbed" because Gogoanime has a different entry for dubbed anime.
|
||||
// e.g. 1$provider$true
|
||||
providerEpisodeListKey := fmt.Sprintf("%d$%s$%v", mId, provider, dubbed)
|
||||
providerEpisodeListKey := fmt.Sprintf("%d$%s$%v", media.ID, provider, dubbed)
|
||||
|
||||
// Create the episode container
|
||||
ec := &episodeContainer{
|
||||
@@ -70,14 +71,14 @@ func (r *Repository) getEpisodeContainer(provider string, mId int, titles []*str
|
||||
Msgf("onlinestream: Fetching %s episode list", provider)
|
||||
|
||||
// Buckets for caching the episode list and episode data.
|
||||
fcEpisodeListBucket := r.getFcEpisodeListBucket(provider, mId)
|
||||
fcEpisodeDataBucket := r.getFcEpisodeDataBucket(provider, mId)
|
||||
fcEpisodeListBucket := r.getFcEpisodeListBucket(provider, media.ID)
|
||||
fcEpisodeDataBucket := r.getFcEpisodeDataBucket(provider, media.ID)
|
||||
|
||||
// Check if the episode list is cached to avoid fetching it again.
|
||||
var providerEpisodeList []*hibikeonlinestream.EpisodeDetails
|
||||
if found, _ := r.fileCacher.Get(fcEpisodeListBucket, providerEpisodeListKey, &providerEpisodeList); !found {
|
||||
var err error
|
||||
providerEpisodeList, err = r.getProviderEpisodeListFromTitles(provider, mId, titles, dubbed, year)
|
||||
providerEpisodeList, err = r.getProviderEpisodeListFromTitles(provider, media, dubbed, year)
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msg("onlinestream: Failed to get provider episodes")
|
||||
return nil, err // ErrNoAnimeFound or ErrNoEpisodes
|
||||
@@ -96,7 +97,7 @@ func (r *Repository) getEpisodeContainer(provider string, mId int, titles []*str
|
||||
if episodeDetails.Number >= from && episodeDetails.Number <= to {
|
||||
|
||||
// Check if the episode is cached to avoid fetching the sources again.
|
||||
key := fmt.Sprintf("%d$%s$%d$%v", mId, provider, episodeDetails.Number, dubbed)
|
||||
key := fmt.Sprintf("%d$%s$%d$%v", media.ID, provider, episodeDetails.Number, dubbed)
|
||||
|
||||
r.logger.Debug().
|
||||
Str("key", key).
|
||||
@@ -195,19 +196,21 @@ func (r *Repository) getProviderEpisodeServers(provider string, episodeDetails *
|
||||
|
||||
// getProviderEpisodeListFromTitles gets all the hibikeonlinestream.EpisodeDetails from the provider based on the anime's titles.
|
||||
// It returns ErrNoAnimeFound if the anime is not found or ErrNoEpisodes if no episodes are found.
|
||||
func (r *Repository) getProviderEpisodeListFromTitles(provider string, mId int, titles []*string, dubbed bool, year int) ([]*hibikeonlinestream.EpisodeDetails, error) {
|
||||
func (r *Repository) getProviderEpisodeListFromTitles(provider string, media *anilist.BaseAnime, dubbed bool, year int) ([]*hibikeonlinestream.EpisodeDetails, error) {
|
||||
var ret []*hibikeonlinestream.EpisodeDetails
|
||||
romajiTitle := strings.ReplaceAll(*titles[0], ":", "")
|
||||
englishTitle := ""
|
||||
if len(titles) > 1 {
|
||||
englishTitle = strings.ReplaceAll(*titles[1], ":", "")
|
||||
}
|
||||
// romajiTitle := strings.ReplaceAll(media.GetEnglishTitleSafe(), ":", "")
|
||||
// englishTitle := strings.ReplaceAll(media.GetRomajiTitleSafe(), ":", "")
|
||||
|
||||
romajiTitle := media.GetRomajiTitleSafe()
|
||||
englishTitle := media.GetEnglishTitleSafe()
|
||||
|
||||
providerExtension, ok := extension.GetExtension[extension.OnlinestreamProviderExtension](r.providerExtensionBank, provider)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("provider extension '%s' not found", provider)
|
||||
}
|
||||
|
||||
mId := media.ID
|
||||
|
||||
var matchId string
|
||||
|
||||
// +---------------------+
|
||||
@@ -229,31 +232,75 @@ func (r *Repository) getProviderEpisodeListFromTitles(provider string, mId int,
|
||||
// Get search results.
|
||||
var searchResults []*hibikeonlinestream.SearchResult
|
||||
|
||||
// Search by romaji title
|
||||
res, err := providerExtension.GetProvider().Search(hibikeonlinestream.SearchOptions{
|
||||
Query: romajiTitle,
|
||||
Dub: dubbed,
|
||||
Year: year,
|
||||
})
|
||||
if err == nil {
|
||||
searchResults = res
|
||||
} else {
|
||||
queryMedia := hibikeonlinestream.Media{
|
||||
ID: media.ID,
|
||||
IDMal: media.GetIDMal(),
|
||||
Status: string(*media.GetStatus()),
|
||||
Format: string(*media.GetFormat()),
|
||||
EnglishTitle: media.GetTitle().GetEnglish(),
|
||||
RomajiTitle: media.GetRomajiTitleSafe(),
|
||||
EpisodeCount: media.GetTotalEpisodeCount(),
|
||||
Synonyms: media.GetSynonymsContainingSeason(),
|
||||
IsAdult: *media.GetIsAdult(),
|
||||
StartDate: &hibikeonlinestream.FuzzyDate{
|
||||
Year: *media.GetStartDate().GetYear(),
|
||||
Month: media.GetStartDate().GetMonth(),
|
||||
Day: media.GetStartDate().GetDay(),
|
||||
},
|
||||
}
|
||||
|
||||
added := make(map[string]struct{})
|
||||
|
||||
if romajiTitle != "" {
|
||||
// Search by romaji title
|
||||
res, err := providerExtension.GetProvider().Search(hibikeonlinestream.SearchOptions{
|
||||
Media: queryMedia,
|
||||
Query: romajiTitle,
|
||||
Dub: dubbed,
|
||||
Year: year,
|
||||
})
|
||||
if err == nil && len(res) > 0 {
|
||||
searchResults = append(searchResults, res...)
|
||||
for _, r := range res {
|
||||
added[r.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msg("onlinestream: Failed to search for romaji title")
|
||||
}
|
||||
r.logger.Debug().
|
||||
Int("romajiTitleResults", len(res)).
|
||||
Msg("onlinestream: Found results for romaji title")
|
||||
}
|
||||
|
||||
if englishTitle != "" {
|
||||
// Search by english title
|
||||
res, err = providerExtension.GetProvider().Search(hibikeonlinestream.SearchOptions{
|
||||
res, err := providerExtension.GetProvider().Search(hibikeonlinestream.SearchOptions{
|
||||
Media: queryMedia,
|
||||
Query: englishTitle,
|
||||
Dub: dubbed,
|
||||
Year: year,
|
||||
})
|
||||
if err == nil {
|
||||
searchResults = res
|
||||
if err == nil && len(res) > 0 {
|
||||
for _, r := range res {
|
||||
if _, ok := added[r.ID]; !ok {
|
||||
searchResults = append(searchResults, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msg("onlinestream: Failed to search for english title")
|
||||
}
|
||||
r.logger.Debug().
|
||||
Int("englishTitleResults", len(res)).
|
||||
Msg("onlinestream: Found results for english title")
|
||||
}
|
||||
|
||||
if len(searchResults) == 0 {
|
||||
return nil, ErrNoAnimeFound
|
||||
}
|
||||
|
||||
bestResult := GetBestSearchResult(searchResults, titles)
|
||||
bestResult := GetBestSearchResult(searchResults, media.GetAllTitles())
|
||||
matchId = bestResult.ID
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ func TestOnlineStream_GetEpisodes(t *testing.T) {
|
||||
}
|
||||
media := mediaF.GetMedia()
|
||||
|
||||
ec, err := os.getEpisodeContainer(tt.provider, tt.mediaId, media.GetAllTitles(), tt.from, tt.to, tt.dubbed, 0)
|
||||
ec, err := os.getEpisodeContainer(tt.provider, media, tt.from, tt.to, tt.dubbed, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("couldn't find episodes, %s", err)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"seanime/internal/library/anime"
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/comparison"
|
||||
"seanime/internal/util/result"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
@@ -28,6 +29,10 @@ const (
|
||||
AnimeSearchTypeSimple AnimeSearchType = "simple"
|
||||
)
|
||||
|
||||
var (
|
||||
metadataCache = result.NewResultMap[string, *TorrentMetadata]()
|
||||
)
|
||||
|
||||
type (
|
||||
AnimeSearchType string
|
||||
|
||||
@@ -51,11 +56,18 @@ type (
|
||||
Torrent *hibiketorrent.AnimeTorrent `json:"torrent"`
|
||||
}
|
||||
|
||||
TorrentMetadata struct {
|
||||
Distance int `json:"distance"`
|
||||
Metadata *habari.Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
// SearchData is the struct returned by NewSmartSearch
|
||||
SearchData struct {
|
||||
Torrents []*hibiketorrent.AnimeTorrent `json:"torrents"` // Torrents found
|
||||
Previews []*Preview `json:"previews"` // TorrentPreview for each torrent
|
||||
TorrentMetadata map[string]*TorrentMetadata `json:"torrentMetadata"` // Torrent metadata
|
||||
DebridInstantAvailability map[string]debrid.TorrentItemInstantAvailability `json:"debridInstantAvailability"` // Debrid instant availability
|
||||
AnimeMetadata *metadata.AnimeMetadata `json:"animeMetadata"` // AniZip media
|
||||
}
|
||||
)
|
||||
|
||||
@@ -193,7 +205,42 @@ func (r *Repository) SearchAnime(ctx context.Context, opts AnimeSearchOptions) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add preview for smart search
|
||||
//
|
||||
// Torrent metadata
|
||||
//
|
||||
torrentMetadata := make(map[string]*TorrentMetadata)
|
||||
mu := sync.Mutex{}
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(torrents))
|
||||
for _, t := range torrents {
|
||||
go func(t *hibiketorrent.AnimeTorrent) {
|
||||
defer wg.Done()
|
||||
metadata, found := metadataCache.Get(t.Name)
|
||||
if !found {
|
||||
m := habari.Parse(t.Name)
|
||||
var distance *comparison.LevenshteinResult
|
||||
distance, ok := comparison.FindBestMatchWithLevenshtein(&m.Title, opts.Media.GetAllTitles())
|
||||
if !ok {
|
||||
distance = &comparison.LevenshteinResult{
|
||||
Distance: 1000,
|
||||
}
|
||||
}
|
||||
metadata = &TorrentMetadata{
|
||||
Distance: distance.Distance,
|
||||
Metadata: m,
|
||||
}
|
||||
metadataCache.Set(t.Name, metadata)
|
||||
}
|
||||
mu.Lock()
|
||||
torrentMetadata[t.InfoHash] = metadata
|
||||
mu.Unlock()
|
||||
}(t)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
//
|
||||
// Previews
|
||||
//
|
||||
previews := make([]*Preview, 0)
|
||||
|
||||
if opts.Type == AnimeSearchTypeSmart {
|
||||
@@ -244,8 +291,13 @@ func (r *Repository) SearchAnime(ctx context.Context, opts AnimeSearchOptions) (
|
||||
})
|
||||
|
||||
ret = &SearchData{
|
||||
Torrents: torrents,
|
||||
Previews: previews,
|
||||
Torrents: torrents,
|
||||
Previews: previews,
|
||||
TorrentMetadata: torrentMetadata,
|
||||
}
|
||||
|
||||
if animeMetadata.IsPresent() {
|
||||
ret.AnimeMetadata = animeMetadata.MustGet()
|
||||
}
|
||||
|
||||
// Store the data in the cache
|
||||
@@ -272,7 +324,16 @@ type createAnimeTorrentPreviewOptions struct {
|
||||
|
||||
func (r *Repository) createAnimeTorrentPreview(opts createAnimeTorrentPreviewOptions) *Preview {
|
||||
|
||||
parsedData := habari.Parse(opts.torrent.Name)
|
||||
var parsedData *habari.Metadata
|
||||
metadata, found := metadataCache.Get(opts.torrent.Name)
|
||||
if !found { // Should always be found
|
||||
parsedData = habari.Parse(opts.torrent.Name)
|
||||
metadataCache.Set(opts.torrent.Name, &TorrentMetadata{
|
||||
Distance: 1000,
|
||||
Metadata: parsedData,
|
||||
})
|
||||
}
|
||||
parsedData = metadata.Metadata
|
||||
|
||||
isBatch := opts.torrent.IsBestRelease ||
|
||||
opts.torrent.IsBatch ||
|
||||
@@ -289,7 +350,6 @@ func (r *Repository) createAnimeTorrentPreview(opts createAnimeTorrentPreviewOpt
|
||||
|
||||
if opts.torrent.FormattedSize == "" {
|
||||
opts.torrent.FormattedSize = humanize.Bytes(uint64(opts.torrent.Size))
|
||||
|
||||
}
|
||||
|
||||
if isBatch {
|
||||
|
||||
28
internal/vendor_habari/vendor_habari.go
Normal file
28
internal/vendor_habari/vendor_habari.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package vendor_habari
|
||||
|
||||
type Metadata struct {
|
||||
SeasonNumber []string `json:"season_number,omitempty"`
|
||||
PartNumber []string `json:"part_number,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
FormattedTitle string `json:"formatted_title,omitempty"`
|
||||
AnimeType []string `json:"anime_type,omitempty"`
|
||||
Year string `json:"year,omitempty"`
|
||||
AudioTerm []string `json:"audio_term,omitempty"`
|
||||
DeviceCompatibility []string `json:"device_compatibility,omitempty"`
|
||||
EpisodeNumber []string `json:"episode_number,omitempty"`
|
||||
OtherEpisodeNumber []string `json:"other_episode_number,omitempty"`
|
||||
EpisodeNumberAlt []string `json:"episode_number_alt,omitempty"`
|
||||
EpisodeTitle string `json:"episode_title,omitempty"`
|
||||
FileChecksum string `json:"file_checksum,omitempty"`
|
||||
FileExtension string `json:"file_extension,omitempty"`
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
Language []string `json:"language,omitempty"`
|
||||
ReleaseGroup string `json:"release_group,omitempty"`
|
||||
ReleaseInformation []string `json:"release_information,omitempty"`
|
||||
ReleaseVersion []string `json:"release_version,omitempty"`
|
||||
Source []string `json:"source,omitempty"`
|
||||
Subtitles []string `json:"subtitles,omitempty"`
|
||||
VideoResolution string `json:"video_resolution,omitempty"`
|
||||
VideoTerm []string `json:"video_term,omitempty"`
|
||||
VolumeNumber []string `json:"volume_number,omitempty"`
|
||||
}
|
||||
@@ -2772,6 +2772,64 @@ export type Mediastream_MediaContainer = {
|
||||
*/
|
||||
export type Mediastream_StreamType = "transcode" | "optimized" | "direct"
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Metadata
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* - Filepath: internal/api/metadata/types.go
|
||||
* - Filename: types.go
|
||||
* - Package: metadata
|
||||
*/
|
||||
export type Metadata_AnimeMappings = {
|
||||
animeplanetId: string
|
||||
kitsuId: number
|
||||
malId: number
|
||||
type: string
|
||||
anilistId: number
|
||||
anisearchId: number
|
||||
anidbId: number
|
||||
notifymoeId: string
|
||||
livechartId: number
|
||||
thetvdbId: number
|
||||
imdbId: string
|
||||
themoviedbId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* - Filepath: internal/api/metadata/types.go
|
||||
* - Filename: types.go
|
||||
* - Package: metadata
|
||||
*/
|
||||
export type Metadata_AnimeMetadata = {
|
||||
titles?: Record<string, string>
|
||||
episodes?: Record<string, Metadata_EpisodeMetadata>
|
||||
episodeCount: number
|
||||
specialCount: number
|
||||
mappings?: Metadata_AnimeMappings
|
||||
}
|
||||
|
||||
/**
|
||||
* - Filepath: internal/api/metadata/types.go
|
||||
* - Filename: types.go
|
||||
* - Package: metadata
|
||||
*/
|
||||
export type Metadata_EpisodeMetadata = {
|
||||
anidbId: number
|
||||
tvdbId: number
|
||||
title: string
|
||||
image: string
|
||||
airDate: string
|
||||
length: number
|
||||
summary: string
|
||||
overview: string
|
||||
episodeNumber: number
|
||||
episode: string
|
||||
seasonNumber: number
|
||||
absoluteEpisodeNumber: number
|
||||
anidbEid: number
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Models
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -3407,10 +3465,28 @@ export type Torrent_SearchData = {
|
||||
* TorrentPreview for each torrent
|
||||
*/
|
||||
previews?: Array<Torrent_Preview>
|
||||
/**
|
||||
* Torrent metadata
|
||||
*/
|
||||
torrentMetadata?: Record<string, Torrent_TorrentMetadata>
|
||||
/**
|
||||
* Debrid instant availability
|
||||
*/
|
||||
debridInstantAvailability?: Record<string, Debrid_TorrentItemInstantAvailability>
|
||||
/**
|
||||
* AniZip media
|
||||
*/
|
||||
animeMetadata?: Metadata_AnimeMetadata
|
||||
}
|
||||
|
||||
/**
|
||||
* - Filepath: internal/torrents/torrent/search.go
|
||||
* - Filename: search.go
|
||||
* - Package: torrent
|
||||
*/
|
||||
export type Torrent_TorrentMetadata = {
|
||||
distance: number
|
||||
metadata?: Habari_Metadata
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -3589,6 +3665,42 @@ export type Updater_Update = {
|
||||
type: string
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// VendorHabari
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* - Filepath: internal/vendor_habari/vendor_habari.go
|
||||
* - Filename: vendor_habari.go
|
||||
* - Package: vendor_habari
|
||||
*/
|
||||
export type Habari_Metadata = {
|
||||
season_number?: Array<string>
|
||||
part_number?: Array<string>
|
||||
title?: string
|
||||
formatted_title?: string
|
||||
anime_type?: Array<string>
|
||||
year?: string
|
||||
audio_term?: Array<string>
|
||||
device_compatibility?: Array<string>
|
||||
episode_number?: Array<string>
|
||||
other_episode_number?: Array<string>
|
||||
episode_number_alt?: Array<string>
|
||||
episode_title?: string
|
||||
file_checksum?: string
|
||||
file_extension?: string
|
||||
file_name?: string
|
||||
language?: Array<string>
|
||||
release_group?: string
|
||||
release_information?: Array<string>
|
||||
release_version?: Array<string>
|
||||
source?: Array<string>
|
||||
subtitles?: Array<string>
|
||||
video_resolution?: string
|
||||
video_term?: Array<string>
|
||||
volume_number?: Array<string>
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Videofile
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Torrent_TorrentMetadata } from "@/api/generated/types"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Tooltip } from "@/components/ui/tooltip"
|
||||
import { startCase } from "lodash"
|
||||
import React from "react"
|
||||
import { LuGauge } from "react-icons/lu"
|
||||
import { BiMicrophone } from "react-icons/bi"
|
||||
import { LiaMicrophoneSolid } from "react-icons/lia"
|
||||
import { LuAudioLines, LuAudioWaveform, LuGauge } from "react-icons/lu"
|
||||
import { PiChat, PiChatCircleTextDuotone, PiChatsTeardropDuotone, PiChatTeardropDuotone } from "react-icons/pi"
|
||||
|
||||
export function TorrentResolutionBadge({ resolution }: { resolution?: string }) {
|
||||
|
||||
@@ -41,13 +46,67 @@ export function TorrentSeedersBadge({ seeders }: { seeders: number }) {
|
||||
}
|
||||
|
||||
|
||||
export function TorrentParsedMetadata({ metadata }: { metadata: Torrent_TorrentMetadata | undefined }) {
|
||||
|
||||
if (!metadata) return null
|
||||
|
||||
const hasDubs = metadata.metadata?.subtitles?.some(n => n.toLocaleLowerCase().includes("dub"))
|
||||
// const hasSubs = metadata.metadata?.subtitles?.some(n => n.toLocaleLowerCase().includes("sub"))
|
||||
const hasMultiSubs = metadata.metadata?.subtitles?.some(n => n.toLocaleLowerCase().includes("multi"))
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-1 flex-wrap justify-end w-full lg:absolute bottom-0 right-0">
|
||||
{metadata.metadata?.video_term?.map(term => (
|
||||
<Badge
|
||||
key={term}
|
||||
className="rounded-md border-transparent bg-[--subtle] !text-[--muted] px-1"
|
||||
>
|
||||
{term}
|
||||
</Badge>
|
||||
))}
|
||||
{!!metadata.metadata?.language?.length ? [...new Set(metadata.metadata?.language)].map(term => (
|
||||
<Badge
|
||||
key={term}
|
||||
className="rounded-md border-transparent bg-[--subtle] px-1"
|
||||
>
|
||||
<PiChatTeardropDuotone className="text-lg text-[--blue]" /> {term}
|
||||
</Badge>
|
||||
)) : null}
|
||||
{metadata.metadata?.audio_term?.filter(term => term.toLowerCase().includes("dual") || term.toLowerCase().includes("multi")).map(term => (
|
||||
<Badge
|
||||
key={term}
|
||||
className="rounded-md border-transparent bg-[--subtle] px-1"
|
||||
>
|
||||
{/* <LuAudioWaveform className="text-lg text-[--blue]" /> {term} */}
|
||||
<PiChatsTeardropDuotone className="text-lg text-[--rose]" /> {startCase(term)}
|
||||
</Badge>
|
||||
))}
|
||||
{hasDubs && (
|
||||
<Badge
|
||||
className="rounded-md border-transparent bg-indigo-300 px-1"
|
||||
>
|
||||
<LiaMicrophoneSolid className="text-lg text-[--red]" /> Dubbed
|
||||
</Badge>
|
||||
)}
|
||||
{hasMultiSubs && (
|
||||
<Badge
|
||||
className="rounded-md border-transparent bg-indigo-300 px-1"
|
||||
>
|
||||
<PiChatCircleTextDuotone className="text-lg text-[--orange]" /> Multi Subs
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function TorrentDebridInstantAvailabilityBadge() {
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
trigger={<Badge
|
||||
data-torrent-item-debrid-instant-availability-badge
|
||||
className="rounded-[--radius-md] bg-transparent dark:text-[--green]"
|
||||
className="rounded-[--radius-md] bg-transparent border-transparent dark:text-[--white] animate-pulse"
|
||||
intent="white"
|
||||
leftIcon={<LuGauge className="text-lg" />}
|
||||
>
|
||||
|
||||
@@ -91,19 +91,21 @@ export const TorrentPreviewItem = memo((props: TorrentPreviewItemProps) => {
|
||||
</div>}
|
||||
|
||||
<div className="absolute left-0 top-0 w-full h-full max-w-[180px]" data-torrent-preview-item-image-container>
|
||||
{(confirmed ? !!image : !!fallbackImage) && <Image
|
||||
{(image || fallbackImage) && <Image
|
||||
data-torrent-preview-item-image
|
||||
src={confirmed ? image! : fallbackImage!}
|
||||
src={image || fallbackImage!}
|
||||
alt="episode image"
|
||||
fill
|
||||
className={cn(
|
||||
"object-cover object-center absolute w-full h-full group-hover/torrent-preview-item:blur-0 transition-opacity opacity-25 group-hover/torrent-preview-item:opacity-60 z-[0] select-none pointer-events-none",
|
||||
(!image && fallbackImage) && "opacity-10 group-hover/torrent-preview-item:opacity-30",
|
||||
isSelected && "opacity-50",
|
||||
|
||||
)}
|
||||
/>}
|
||||
<div
|
||||
data-torrent-preview-item-image-bottom-gradient
|
||||
className="transition-colors absolute w-full h-full bg-gradient-to-l from-[--background] hover:from-[var(--hover-from-background-color)] to-transparent z-[1] select-none pointer-events-none"
|
||||
data-torrent-preview-item-image-end-gradient
|
||||
className="transition-colors absolute w-full h-full -right-2 bg-gradient-to-l from-[--background] hover:from-[var(--hover-from-background-color)] to-transparent z-[1] select-none pointer-events-none"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -144,13 +146,14 @@ export const TorrentPreviewItem = memo((props: TorrentPreviewItemProps) => {
|
||||
{!(image || fallbackImage) && !isBatch && <BsFileEarmarkPlayFill className="text-7xl absolute opacity-10" />}
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-hidden space-y-1" data-torrent-preview-item-metadata>
|
||||
<div className="relative overflow-hidden space-y-1 w-full" data-torrent-preview-item-metadata>
|
||||
{isInvalid && <p className="flex gap-2 text-red-300 items-center"><AiFillWarning
|
||||
className="text-lg text-red-500"
|
||||
/> Unidentified</p>}
|
||||
|
||||
<p
|
||||
className={cn(
|
||||
"font-medium text-base transition line-clamp-2 tracking-wider",
|
||||
"font-normal text-[.9rem] transition line-clamp-2 tracking-wide",
|
||||
isBasic && "text-sm",
|
||||
)}
|
||||
data-torrent-preview-item-title
|
||||
@@ -158,15 +161,15 @@ export const TorrentPreviewItem = memo((props: TorrentPreviewItemProps) => {
|
||||
|
||||
{!!subtitle && <p
|
||||
className={cn(
|
||||
"text-sm tracking-wide group-hover/torrent-preview-item:text-gray-200 line-clamp-2 break-all",
|
||||
!(_title) ? "font-medium transition tracking-wider" : "text-[--muted]",
|
||||
"text-[.85rem] tracking-wide group-hover/torrent-preview-item:text-gray-200 line-clamp-2 break-all",
|
||||
!(_title) ? "font-normal transition tracking-wide" : "text-[--muted]",
|
||||
)}
|
||||
data-torrent-preview-item-subtitle
|
||||
>
|
||||
{subtitle}
|
||||
</p>}
|
||||
|
||||
<div className="flex items-center gap-2" data-torrent-preview-item-subcontent>
|
||||
<div className="flex flex-col gap-2" data-torrent-preview-item-subcontent>
|
||||
{children && children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anime_Entry, Debrid_TorrentItemInstantAvailability, HibikeTorrent_AnimeTorrent, Torrent_Preview } from "@/api/generated/types"
|
||||
import { Anime_Entry, Debrid_TorrentItemInstantAvailability, HibikeTorrent_AnimeTorrent, Torrent_Preview, Torrent_TorrentMetadata } from "@/api/generated/types"
|
||||
import {
|
||||
TorrentDebridInstantAvailabilityBadge,
|
||||
TorrentParsedMetadata,
|
||||
TorrentResolutionBadge,
|
||||
TorrentSeedersBadge,
|
||||
} from "@/app/(main)/entry/_containers/torrent-search/_components/torrent-item-badges"
|
||||
@@ -29,6 +30,7 @@ type TorrentPreviewList = {
|
||||
selectedTorrents: HibikeTorrent_AnimeTorrent[]
|
||||
onToggleTorrent: (t: HibikeTorrent_AnimeTorrent) => void
|
||||
type: TorrentSelectionType
|
||||
torrentMetadata: Record<string, Torrent_TorrentMetadata> | undefined
|
||||
}
|
||||
|
||||
export const TorrentPreviewList = React.memo((
|
||||
@@ -40,6 +42,7 @@ export const TorrentPreviewList = React.memo((
|
||||
onToggleTorrent,
|
||||
debridInstantAvailability,
|
||||
type,
|
||||
torrentMetadata,
|
||||
}: TorrentPreviewList) => {
|
||||
// Add sorting state
|
||||
const [sortField, setSortField] = useState<SortField>("seeders")
|
||||
@@ -133,7 +136,7 @@ export const TorrentPreviewList = React.memo((
|
||||
isSelected={selectedTorrents.findIndex(n => n.link === item.torrent!.link) !== -1}
|
||||
onClick={() => onToggleTorrent(item.torrent!)}
|
||||
>
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{item.torrent.isBestRelease && (
|
||||
<Badge
|
||||
className="rounded-[--radius-md] text-[0.8rem] bg-pink-800 border-transparent border"
|
||||
@@ -153,6 +156,7 @@ export const TorrentPreviewList = React.memo((
|
||||
<BiCalendarAlt /> {formatDistanceToNowSafe(item.torrent.date)}
|
||||
</p>
|
||||
</div>
|
||||
<TorrentParsedMetadata metadata={torrentMetadata?.[item.torrent.infoHash!]} />
|
||||
</TorrentPreviewItem>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Anime_Entry, Debrid_TorrentItemInstantAvailability, HibikeTorrent_AnimeTorrent } from "@/api/generated/types"
|
||||
import { Anime_Entry, Debrid_TorrentItemInstantAvailability, HibikeTorrent_AnimeTorrent, Metadata_AnimeMetadata, Torrent_TorrentMetadata } from "@/api/generated/types"
|
||||
import {
|
||||
TorrentDebridInstantAvailabilityBadge,
|
||||
TorrentParsedMetadata,
|
||||
TorrentResolutionBadge,
|
||||
TorrentSeedersBadge,
|
||||
} from "@/app/(main)/entry/_containers/torrent-search/_components/torrent-item-badges"
|
||||
@@ -34,6 +35,8 @@ type TorrentTable = {
|
||||
isFetching: boolean
|
||||
onToggleTorrent: (t: HibikeTorrent_AnimeTorrent) => void
|
||||
debridInstantAvailability: Record<string, Debrid_TorrentItemInstantAvailability>
|
||||
animeMetadata: Metadata_AnimeMetadata | undefined
|
||||
torrentMetadata: Record<string, Torrent_TorrentMetadata> | undefined
|
||||
}
|
||||
|
||||
export const TorrentTable = memo((
|
||||
@@ -49,6 +52,8 @@ export const TorrentTable = memo((
|
||||
isLoading,
|
||||
onToggleTorrent,
|
||||
debridInstantAvailability,
|
||||
animeMetadata,
|
||||
torrentMetadata,
|
||||
}: TorrentTable) => {
|
||||
// Add sorting state
|
||||
const [sortField, setSortField] = useState<SortField>("seeders")
|
||||
@@ -124,25 +129,54 @@ export const TorrentTable = memo((
|
||||
</div>
|
||||
<ScrollAreaBox className="h-[calc(100dvh_-_25rem)]">
|
||||
{sortedTorrents.map(torrent => {
|
||||
let episodeNumber = torrent.episodeNumber || -1
|
||||
let totalEpisodes = entry?.media?.episodes || (entry?.media?.nextAiringEpisode?.episode ? entry?.media?.nextAiringEpisode?.episode : 0)
|
||||
if (episodeNumber > totalEpisodes) {
|
||||
// normalize episode number
|
||||
for (const epKey in animeMetadata?.episodes) {
|
||||
const ep = animeMetadata?.episodes?.[epKey]
|
||||
if (ep?.absoluteEpisodeNumber === episodeNumber) {
|
||||
episodeNumber = ep.episodeNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let episodeImage: string | undefined
|
||||
if (!!animeMetadata && (episodeNumber ?? -1) >= 0) {
|
||||
const episode = animeMetadata.episodes?.[episodeNumber!.toString()]
|
||||
if (episode) {
|
||||
episodeImage = episode.image
|
||||
}
|
||||
}
|
||||
let distance = 9999
|
||||
if (!!torrentMetadata && !!torrent.infoHash) {
|
||||
const metadata = torrentMetadata[torrent.infoHash!]
|
||||
if (metadata) {
|
||||
distance = metadata.distance
|
||||
}
|
||||
}
|
||||
if (distance > 10) {
|
||||
episodeImage = undefined
|
||||
}
|
||||
return (
|
||||
<TorrentPreviewItem
|
||||
isBasic
|
||||
// isBasic
|
||||
link={torrent.link}
|
||||
key={torrent.link}
|
||||
title={torrent.name}
|
||||
releaseGroup={torrent.releaseGroup || ""}
|
||||
subtitle={torrent.isBatch ? torrent.name : (torrent?.episodeNumber || -1) >= 0
|
||||
? `Episode ${torrent?.episodeNumber ?? "N/A"}`
|
||||
subtitle={torrent.isBatch ? torrent.name : (episodeNumber ?? -1) >= 0
|
||||
? `Episode ${episodeNumber}`
|
||||
: ""}
|
||||
isBatch={torrent.isBatch ?? false}
|
||||
isBestRelease={torrent.isBestRelease}
|
||||
// image={item.episode?.episodeMetadata?.image || item.episode?.baseAnime?.coverImage?.large ||
|
||||
// (torrent.confirmed ? (entry.media?.coverImage?.large || entry.media?.bannerImage) : null)}
|
||||
// fallbackImage={entry.media?.coverImage?.large || entry.media?.bannerImage}
|
||||
image={distance <= 10 ? episodeImage : undefined}
|
||||
fallbackImage={(entry?.media?.coverImage?.large || entry?.media?.bannerImage)}
|
||||
isSelected={selectedTorrents.findIndex(n => n.link === torrent!.link) !== -1}
|
||||
onClick={() => onToggleTorrent(torrent!)}
|
||||
// confirmed={distance === 0}
|
||||
>
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{torrent.isBestRelease && (
|
||||
<Badge
|
||||
className="rounded-[--radius-md] text-[0.8rem] bg-pink-800 border-transparent border"
|
||||
@@ -162,6 +196,7 @@ export const TorrentTable = memo((
|
||||
<BiCalendarAlt /> {formatDistanceToNowSafe(torrent.date)}
|
||||
</p>
|
||||
</div>
|
||||
<TorrentParsedMetadata metadata={torrentMetadata?.[torrent.infoHash!]} />
|
||||
</TorrentPreviewItem>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -347,6 +347,8 @@ export function TorrentSearchContainer({ type, entry }: { type: TorrentSelection
|
||||
onToggleTorrent={handleToggleTorrent}
|
||||
debridInstantAvailability={debridInstantAvailability}
|
||||
type={type}
|
||||
torrentMetadata={data?.torrentMetadata}
|
||||
// animeMetadata={data?.animeMetadata}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -357,6 +359,7 @@ export function TorrentSearchContainer({ type, entry }: { type: TorrentSelection
|
||||
{((searchType !== Torrent_SearchType.SMART) && !hasOneWarning && !previews?.length) && (
|
||||
<>
|
||||
<TorrentTable
|
||||
entry={entry}
|
||||
torrents={torrents}
|
||||
globalFilter={globalFilter}
|
||||
setGlobalFilter={setGlobalFilter}
|
||||
@@ -367,6 +370,8 @@ export function TorrentSearchContainer({ type, entry }: { type: TorrentSelection
|
||||
selectedTorrents={selectedTorrents}
|
||||
onToggleTorrent={handleToggleTorrent}
|
||||
debridInstantAvailability={debridInstantAvailability}
|
||||
animeMetadata={data?.animeMetadata}
|
||||
torrentMetadata={data?.torrentMetadata}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user