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,259 @@
package nyaa
import (
"fmt"
gourl "net/url"
"seanime/internal/util"
)
type (
Torrent struct {
Category string `json:"category"`
Name string `json:"name"`
Description string `json:"description"`
Date string `json:"date"`
Size string `json:"size"`
Seeders string `json:"seeders"`
Leechers string `json:"leechers"`
Downloads string `json:"downloads"`
IsTrusted string `json:"isTrusted"`
IsRemake string `json:"isRemake"`
Comments string `json:"comments"`
Link string `json:"link"`
GUID string `json:"guid"`
CategoryID string `json:"categoryID"`
InfoHash string `json:"infoHash"`
}
BuildURLOptions struct {
Provider string
Query string
Category string
SortBy string
Filter string
}
Comment struct {
User string `json:"user"`
Date string `json:"date"`
Text string `json:"text"`
}
)
func (t *Torrent) GetSizeInBytes() int64 {
bytes, _ := util.StringSizeToBytes(t.Size)
return bytes
}
var (
nyaaBaseURL = util.Decode("aHR0cHM6Ly9ueWFhLnNpLz9wYWdlPXJzcyZxPSs=")
sukebeiBaseURL = util.Decode("aHR0cHM6Ly9zdWtlYmVpLm55YWEuc2kvP3BhZ2U9cnNzJnE9Kw==")
nyaaView = util.Decode("aHR0cHM6Ly9ueWFhLnNpL3ZpZXcv")
sukebeiView = util.Decode("aHR0cHM6Ly9zdWtlYmVpLm55YWEuc2kvdmlldy8=")
)
const (
sortByComments = "&s=comments&o=desc"
sortBySeeders = "&s=seeders&o=desc"
sortByLeechers = "&s=leechers&o=desc"
sortByDownloads = "&s=downloads&o=desc"
sortBySizeDsc = "&s=size&o=desc"
sortBySizeAsc = "&s=size&o=asc"
sortByDate = "&s=id&o=desc"
filterNoFilter = "&f=0"
filterNoRemakes = "&f=1"
filterTrustedOnly = "&f=2"
categoryAll = "&c=0_0"
categoryAnime = "&c=1_0"
CategoryAnime = "&c=1_0"
categoryAnimeAMV = "&c=1_1"
categoryAnimeEng = "&c=1_2"
CategoryAnimeEng = "&c=1_2"
categoryAnimeNonEng = "&c=1_3"
CategoryAnimeNonEng = "&c=1_3"
categoryAnimeRaw = "&c=1_4"
categoryAudio = "&c=2_0"
categoryAudioLossless = "&c=2_1"
categoryAudioLossy = "&c=2_2"
categoryLiterature = "&c=3_0"
categoryLiteratureEng = "&c=3_1"
categoryLiteratureNonEng = "&c=3_2"
categoryLiteratureRaw = "&c=3_3"
categoryLiveAction = "&c=4_0"
categoryLiveActionRaw = "&c=4_4"
categoryLiveActionEng = "&c=4_1"
categoryLiveActionNonEng = "&c=4_3"
categoryLiveActionIdolProm = "&c=4_2"
categoryPictures = "&c=5_0"
categoryPicturesGraphics = "&c=5_1"
categoryPicturesPhotos = "&c=5_2"
categorySoftware = "&c=6_0"
categorySoftwareApps = "&c=6_1"
categorySoftwareGames = "&c=6_2"
categoryArt = "&c=1_0"
categoryArtAnime = "&c=1_1"
categoryArtDoujinshi = "&c=1_2"
categoryArtGames = "&c=1_3"
categoryArtManga = "&c=1_4"
categoryArtPictures = "&c=1_5"
categoryRealLife = "&c=2_0"
categoryRealLifePhotos = "&c=2_1"
categoryRealLifeVideos = "&c=2_2"
)
func buildURL(baseUrl string, opts BuildURLOptions) (string, error) {
var url string
if baseUrl == "" {
if opts.Provider == "nyaa" {
url = nyaaBaseURL
} else if opts.Provider == "sukebei" {
url = sukebeiBaseURL
} else {
err := fmt.Errorf("provider option could be nyaa or sukebei")
return "", err
}
} else {
url = baseUrl
}
if opts.Query != "" {
url += gourl.QueryEscape(opts.Query)
}
if opts.Provider == "nyaa" {
if opts.Category != "" {
switch opts.Category {
case "all":
url += categoryAll
case "anime":
url += categoryAnime
case "anime-amv":
url += categoryAnimeAMV
case "anime-eng":
url += categoryAnimeEng
case "anime-non-eng":
url += categoryAnimeNonEng
case "anime-raw":
url += categoryAnimeRaw
case "audio":
url += categoryAudio
case "audio-lossless":
url += categoryAudioLossless
case "audio-lossy":
url += categoryAudioLossy
case "literature":
url += categoryLiterature
case "literature-eng":
url += categoryLiteratureEng
case "literature-non-eng":
url += categoryLiteratureNonEng
case "literature-raw":
url += categoryLiteratureRaw
case "live-action":
url += categoryLiveAction
case "live-action-raw":
url += categoryLiveActionRaw
case "live-action-eng":
url += categoryLiveActionEng
case "live-action-non-eng":
url += categoryLiveActionNonEng
case "live-action-idol-prom":
url += categoryLiveActionIdolProm
case "pictures":
url += categoryPictures
case "pictures-graphics":
url += categoryPicturesGraphics
case "pictures-photos":
url += categoryPicturesPhotos
case "software":
url += categorySoftware
case "software-apps":
url += categorySoftwareApps
case "software-games":
url += categorySoftwareGames
default:
err := fmt.Errorf("such nyaa category option does not exitst")
return "", err
}
}
}
if opts.Provider == "sukebei" {
if opts.Category != "" {
switch opts.Category {
case "all":
url += categoryAll
case "art":
url += categoryArt
case "art-anime":
url += categoryArtAnime
case "art-doujinshi":
url += categoryArtDoujinshi
case "art-games":
url += categoryArtGames
case "art-manga":
url += categoryArtManga
case "art-pictures":
url += categoryArtPictures
case "real-life":
url += categoryRealLife
case "real-life-photos":
url += categoryRealLifePhotos
case "real-life-videos":
url += categoryRealLifeVideos
default:
err := fmt.Errorf("such sukebei category option does not exitst")
return "", err
}
}
}
if opts.SortBy != "" {
switch opts.SortBy {
case "downloads":
url += sortByDownloads
case "comments":
url += sortByComments
case "seeders":
url += sortBySeeders
case "leechers":
url += sortByLeechers
case "size-asc":
url += sortBySizeAsc
case "size-dsc":
url += sortBySizeDsc
case "date":
url += sortByDate
default:
err := fmt.Errorf("such sort option does not exitst")
return "", err
}
}
if opts.Filter != "" {
switch opts.Filter {
case "no-filter":
url += filterNoFilter
case "no-remakes":
url += filterNoRemakes
case "trusted-only":
url += filterTrustedOnly
default:
err := fmt.Errorf("such filter option does not exitst")
return "", err
}
}
return url, nil
}

View File

@@ -0,0 +1,569 @@
package nyaa
import (
"bytes"
"fmt"
"seanime/internal/api/anilist"
"seanime/internal/extension"
hibiketorrent "seanime/internal/extension/hibike/torrent"
"seanime/internal/util"
"seanime/internal/util/comparison"
"strconv"
"strings"
"sync"
"time"
"github.com/5rahim/habari"
"github.com/mmcdole/gofeed"
"github.com/rs/zerolog"
"github.com/samber/lo"
)
const (
NyaaProviderName = "nyaa"
)
type Provider struct {
logger *zerolog.Logger
category string
baseUrl string
}
func NewProvider(logger *zerolog.Logger, category string) hibiketorrent.AnimeProvider {
return &Provider{
logger: logger,
category: category,
}
}
func (n *Provider) SetSavedUserConfig(config extension.SavedUserConfig) {
n.baseUrl, _ = config.Values["apiUrl"]
}
func (n *Provider) GetSettings() hibiketorrent.AnimeProviderSettings {
return hibiketorrent.AnimeProviderSettings{
Type: hibiketorrent.AnimeProviderTypeMain,
CanSmartSearch: true,
SmartSearchFilters: []hibiketorrent.AnimeProviderSmartSearchFilter{
hibiketorrent.AnimeProviderSmartSearchFilterBatch,
hibiketorrent.AnimeProviderSmartSearchFilterEpisodeNumber,
hibiketorrent.AnimeProviderSmartSearchFilterResolution,
hibiketorrent.AnimeProviderSmartSearchFilterQuery,
},
SupportsAdult: false,
}
}
func (n *Provider) GetLatest() (ret []*hibiketorrent.AnimeTorrent, err error) {
fp := gofeed.NewParser()
url, err := buildURL(n.baseUrl, BuildURLOptions{
Provider: "nyaa",
Query: "",
Category: n.category,
SortBy: "seeders",
Filter: "",
})
if err != nil {
return nil, err
}
// get content
feed, err := fp.ParseURL(url)
if err != nil {
return nil, err
}
// parse content
res := convertRSS(feed)
ret = torrentSliceToAnimeTorrentSlice(res, NyaaProviderName)
return
}
func (n *Provider) Search(opts hibiketorrent.AnimeSearchOptions) (ret []*hibiketorrent.AnimeTorrent, err error) {
fp := gofeed.NewParser()
n.logger.Trace().Str("query", opts.Query).Msg("nyaa: Search query")
url, err := buildURL(n.baseUrl, BuildURLOptions{
Provider: "nyaa",
Query: opts.Query,
Category: n.category,
SortBy: "seeders",
Filter: "",
})
if err != nil {
return nil, err
}
// get content
feed, err := fp.ParseURL(url)
if err != nil {
return nil, err
}
// parse content
res := convertRSS(feed)
ret = torrentSliceToAnimeTorrentSlice(res, NyaaProviderName)
return
}
func (n *Provider) SmartSearch(opts hibiketorrent.AnimeSmartSearchOptions) (ret []*hibiketorrent.AnimeTorrent, err error) {
queries, ok := buildSmartSearchQueries(&opts)
if !ok {
return nil, fmt.Errorf("could not build queries")
}
wg := sync.WaitGroup{}
mu := sync.Mutex{}
for _, query := range queries {
wg.Add(1)
go func(query string) {
defer wg.Done()
fp := gofeed.NewParser()
n.logger.Trace().Str("query", query).Msg("nyaa: Smart search query")
url, err := buildURL(n.baseUrl, BuildURLOptions{
Provider: "nyaa",
Query: query,
Category: n.category,
SortBy: "seeders",
Filter: "",
})
if err != nil {
return
}
n.logger.Trace().Str("url", url).Msg("nyaa: Smart search url")
// get content
feed, err := fp.ParseURL(url)
if err != nil {
return
}
// parse content
res := convertRSS(feed)
mu.Lock()
ret = torrentSliceToAnimeTorrentSlice(res, NyaaProviderName)
mu.Unlock()
}(query)
}
wg.Wait()
// remove duplicates
lo.UniqBy(ret, func(i *hibiketorrent.AnimeTorrent) string {
return i.Link
})
if !opts.Batch {
// Single-episode search
// If the episode number is provided, we can filter the results
ret = lo.Filter(ret, func(i *hibiketorrent.AnimeTorrent, _ int) bool {
relEp := i.EpisodeNumber
if relEp == -1 {
return false
}
absEp := opts.Media.AbsoluteSeasonOffset + opts.EpisodeNumber
return opts.EpisodeNumber == relEp || absEp == relEp
})
}
return
}
func (n *Provider) GetTorrentInfoHash(torrent *hibiketorrent.AnimeTorrent) (string, error) {
return torrent.InfoHash, nil
}
func (n *Provider) GetTorrentMagnetLink(torrent *hibiketorrent.AnimeTorrent) (string, error) {
return TorrentMagnet(torrent.Link)
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ADVANCED SEARCH
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// buildSmartSearchQueries will return a slice of queries for nyaa.si.
// The second index of the returned slice is the absolute episode query.
// If the function returns false, the query could not be built.
// BuildSearchQueryOptions.Title will override the constructed title query but not other parameters.
func buildSmartSearchQueries(opts *hibiketorrent.AnimeSmartSearchOptions) ([]string, bool) {
romTitle := opts.Media.RomajiTitle
engTitle := opts.Media.EnglishTitle
allTitles := []*string{&romTitle, engTitle}
for _, synonym := range opts.Media.Synonyms {
allTitles = append(allTitles, &synonym)
}
season := 0
part := 0
// create titles by extracting season/part info
titles := make([]string, 0)
// Build titles if no query provided
if opts.Query == "" {
for _, title := range allTitles {
if title == nil {
continue
}
s, cTitle := util.ExtractSeasonNumber(*title)
p, cTitle := util.ExtractPartNumber(cTitle)
if s != 0 { // update season if it got parsed
season = s
}
if p != 0 { // update part if it got parsed
part = p
}
if cTitle != "" { // add "cleaned" titles
titles = append(titles, cTitle)
}
}
// Check season from synonyms, only update season if it's still 0
for _, synonym := range opts.Media.Synonyms {
s, _ := util.ExtractSeasonNumber(synonym)
if s != 0 && season == 0 {
season = s
}
}
// no season or part got parsed, meaning there is no "cleaned" title,
// add romaji and english titles to the title list
if season == 0 && part == 0 {
titles = append(titles, romTitle)
if engTitle != nil {
if len(*engTitle) > 0 {
titles = append(titles, *engTitle)
}
}
}
// convert III and II to season
// these will get cleaned later
if season == 0 && (strings.Contains(strings.ToLower(romTitle), " iii")) {
season = 3
}
if season == 0 && (strings.Contains(strings.ToLower(romTitle), " ii")) {
season = 2
}
if engTitle != nil {
if season == 0 && (strings.Contains(strings.ToLower(*engTitle), " iii")) {
season = 3
}
if season == 0 && (strings.Contains(strings.ToLower(*engTitle), " ii")) {
season = 2
}
}
// also, split romaji title by colon,
// if first part is long enough, add it to the title list
// DEVNOTE maybe we should only do that if the season IS found
split := strings.Split(romTitle, ":")
if len(split) > 1 && len(split[0]) > 8 {
titles = append(titles, split[0])
}
if engTitle != nil {
split := strings.Split(*engTitle, ":")
if len(split) > 1 && len(split[0]) > 8 {
titles = append(titles, split[0])
}
}
// clean titles
for i, title := range titles {
titles[i] = strings.TrimSpace(strings.ReplaceAll(title, ":", " "))
titles[i] = strings.TrimSpace(strings.ReplaceAll(titles[i], "-", " "))
titles[i] = strings.Join(strings.Fields(titles[i]), " ")
titles[i] = strings.ToLower(titles[i])
if season != 0 {
titles[i] = strings.ReplaceAll(titles[i], " iii", "")
titles[i] = strings.ReplaceAll(titles[i], " ii", "")
}
}
titles = lo.Uniq(titles)
} else {
titles = append(titles, strings.ToLower(opts.Query))
}
//
// Parameters
//
// can batch if media stopped airing
canBatch := false
if opts.Media.Status == string(anilist.MediaStatusFinished) && opts.Media.EpisodeCount > 0 {
canBatch = true
}
normalBuff := bytes.NewBufferString("")
// Batch section - empty unless:
// 1. If the media is finished and has more than 1 episode
// 2. If the media is not a movie
// 3. If the media is not a single episode
batchBuff := bytes.NewBufferString("")
if opts.Batch && canBatch && !(opts.Media.Format == string(anilist.MediaFormatMovie) && opts.Media.EpisodeCount == 1) {
if season != 0 {
batchBuff.WriteString(buildSeasonString(season))
}
if part != 0 {
batchBuff.WriteString(buildPartString(part))
}
batchBuff.WriteString(buildBatchString(&opts.Media))
} else {
normalBuff.WriteString(buildSeasonString(season))
if part != 0 {
normalBuff.WriteString(buildPartString(part))
}
if !(opts.Media.Format == string(anilist.MediaFormatMovie) && opts.Media.EpisodeCount == 1) {
normalBuff.WriteString(buildEpisodeString(opts.EpisodeNumber))
}
}
titleStr := buildTitleString(titles)
batchStr := batchBuff.String()
normalStr := normalBuff.String()
// Replace titleStr if user provided one
if opts.Query != "" {
titleStr = fmt.Sprintf(`(%s)`, opts.Query)
}
//println(spew.Sdump(titleStr, batchStr, normalStr))
query := fmt.Sprintf("%s%s%s", titleStr, batchStr, normalStr)
if opts.Resolution != "" {
query = fmt.Sprintf("%s(%s)", query, opts.Resolution)
} else {
query = fmt.Sprintf("%s(%s)", query, strings.Join([]string{"360", "480", "720", "1080"}, "|"))
}
query2 := ""
// Absolute episode addition
if !opts.Batch && opts.Media.AbsoluteSeasonOffset > 0 && !(opts.Media.Format == string(anilist.MediaFormatMovie) && opts.Media.EpisodeCount == 1) {
query2 = fmt.Sprintf("%s", buildAbsoluteGroupString(titleStr, opts.Resolution, opts)) // e.g. jujutsu kaisen 25
}
ret := []string{query}
if query2 != "" {
ret = append(ret, query2)
}
return ret, true
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//func sanitizeTitle(t string) string {
// return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(t, "!", ""), ":", ""), "[", ""), "]", ""), ".", "")
//}
// (title)
// ("jjk"|"jujutsu kaisen")
func buildTitleString(titles []string) string {
// Single titles are not wrapped in quotes
if len(titles) == 1 {
return fmt.Sprintf(`(%s)`, titles[0])
}
return fmt.Sprintf("(%s)", strings.Join(lo.Map(titles, func(item string, _ int) string {
return fmt.Sprintf(`"%s"`, item)
}), "|"))
}
func buildAbsoluteGroupString(title, resolution string, opts *hibiketorrent.AnimeSmartSearchOptions) string {
return fmt.Sprintf("%s(%d)(%s)", title, opts.EpisodeNumber+opts.Media.AbsoluteSeasonOffset, resolution)
}
// (s01e01)
func buildSeasonAndEpisodeGroup(season int, ep int) string {
if season == 0 {
season = 1
}
return fmt.Sprintf(`"s%se%s"`, zeropad(season), zeropad(ep))
}
// (01|e01|e01v|ep01|ep1)
func buildEpisodeString(ep int) string {
pEp := zeropad(ep)
//return fmt.Sprintf(`("%s"|"e%s"|"e%sv"|"%sv"|"ep%s"|"ep%d")`, pEp, pEp, pEp, pEp, pEp, ep)
return fmt.Sprintf(`(%s|e%s|e%sv|%sv|ep%s|ep%d)`, pEp, pEp, pEp, pEp, pEp, ep)
}
// (season 1|season 01|s1|s01)
func buildSeasonString(season int) string {
// Season section
seasonBuff := bytes.NewBufferString("")
// e.g. S1, season 1, season 01
if season != 0 {
seasonBuff.WriteString(fmt.Sprintf(`("%s%d"|`, "season ", season))
seasonBuff.WriteString(fmt.Sprintf(`"%s%s"|`, "season ", zeropad(season)))
seasonBuff.WriteString(fmt.Sprintf(`"%s%d"|`, "s", season))
seasonBuff.WriteString(fmt.Sprintf(`"%s%s")`, "s", zeropad(season)))
}
return seasonBuff.String()
}
func buildPartString(part int) string {
partBuff := bytes.NewBufferString("")
if part != 0 {
partBuff.WriteString(fmt.Sprintf(`("%s%d")`, "part ", part))
}
return partBuff.String()
}
func buildBatchString(m *hibiketorrent.Media) string {
buff := bytes.NewBufferString("")
buff.WriteString("(")
// e.g. 01-12
s1 := fmt.Sprintf(`"%s%s%s"`, zeropad("1"), " - ", zeropad(m.EpisodeCount))
buff.WriteString(s1)
buff.WriteString("|")
// e.g. 01~12
s2 := fmt.Sprintf(`"%s%s%s"`, zeropad("1"), " ~ ", zeropad(m.EpisodeCount))
buff.WriteString(s2)
buff.WriteString("|")
// e.g. 01~12
buff.WriteString(`"Batch"|`)
buff.WriteString(`"Complete"|`)
buff.WriteString(`"+ OVA"|`)
buff.WriteString(`"+ Specials"|`)
buff.WriteString(`"+ Special"|`)
buff.WriteString(`"Seasons"|`)
buff.WriteString(`"Parts"`)
buff.WriteString(")")
return buff.String()
}
func zeropad(v interface{}) string {
switch i := v.(type) {
case int:
return fmt.Sprintf("%02d", i)
case string:
return fmt.Sprintf("%02s", i)
default:
return ""
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func convertRSS(feed *gofeed.Feed) []Torrent {
var res []Torrent
for _, item := range feed.Items {
res = append(
res,
Torrent{
Name: item.Title,
Link: item.Link,
Date: item.Published,
Description: item.Description,
GUID: item.GUID,
Comments: item.Extensions["nyaa"]["comments"][0].Value,
IsTrusted: item.Extensions["nyaa"]["trusted"][0].Value,
IsRemake: item.Extensions["nyaa"]["remake"][0].Value,
Size: item.Extensions["nyaa"]["size"][0].Value,
Seeders: item.Extensions["nyaa"]["seeders"][0].Value,
Leechers: item.Extensions["nyaa"]["leechers"][0].Value,
Downloads: item.Extensions["nyaa"]["downloads"][0].Value,
Category: item.Extensions["nyaa"]["category"][0].Value,
CategoryID: item.Extensions["nyaa"]["categoryId"][0].Value,
InfoHash: item.Extensions["nyaa"]["infoHash"][0].Value,
},
)
}
return res
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func torrentSliceToAnimeTorrentSlice(torrents []Torrent, providerName string) []*hibiketorrent.AnimeTorrent {
wg := sync.WaitGroup{}
mu := sync.Mutex{}
ret := make([]*hibiketorrent.AnimeTorrent, 0)
for _, torrent := range torrents {
wg.Add(1)
go func(torrent Torrent) {
defer wg.Done()
mu.Lock()
ret = append(ret, torrent.toAnimeTorrent(providerName))
mu.Unlock()
}(torrent)
}
wg.Wait()
return ret
}
func (t *Torrent) toAnimeTorrent(providerName string) *hibiketorrent.AnimeTorrent {
metadata := habari.Parse(t.Name)
seeders, _ := strconv.Atoi(t.Seeders)
leechers, _ := strconv.Atoi(t.Leechers)
downloads, _ := strconv.Atoi(t.Downloads)
formattedDate := ""
parsedDate, err := time.Parse("Mon, 02 Jan 2006 15:04:05 -0700", t.Date)
if err == nil {
formattedDate = parsedDate.Format(time.RFC3339)
}
ret := &hibiketorrent.AnimeTorrent{
Name: t.Name,
Date: formattedDate,
Size: t.GetSizeInBytes(),
FormattedSize: t.Size,
Seeders: seeders,
Leechers: leechers,
DownloadCount: downloads,
Link: t.GUID,
DownloadUrl: t.Link,
InfoHash: t.InfoHash,
MagnetLink: "", // Should be scraped
Resolution: "", // Should be parsed
IsBatch: false, // Should be parsed
EpisodeNumber: -1, // Should be parsed
ReleaseGroup: "", // Should be parsed
Provider: providerName,
IsBestRelease: false,
Confirmed: false,
}
isBatchByGuess := false
episode := -1
if len(metadata.EpisodeNumber) > 1 || comparison.ValueContainsBatchKeywords(t.Name) {
isBatchByGuess = true
}
if len(metadata.EpisodeNumber) == 1 {
episode = util.StringToIntMust(metadata.EpisodeNumber[0])
}
ret.Resolution = metadata.VideoResolution
ret.ReleaseGroup = metadata.ReleaseGroup
// Only change batch status if it wasn't already 'true'
if ret.IsBatch == false && isBatchByGuess {
ret.IsBatch = true
}
ret.EpisodeNumber = episode
return ret
}

View File

@@ -0,0 +1,167 @@
package nyaa
import (
"seanime/internal/api/anilist"
hibiketorrent "seanime/internal/extension/hibike/torrent"
"seanime/internal/platforms/anilist_platform"
"seanime/internal/util"
"seanime/internal/util/limiter"
"testing"
"github.com/stretchr/testify/require"
)
func TestSearch(t *testing.T) {
nyaaProvider := NewProvider(util.NewLogger(), categoryAnime)
torrents, err := nyaaProvider.Search(hibiketorrent.AnimeSearchOptions{
Query: "One Piece",
})
require.NoError(t, err)
for _, torrent := range torrents {
t.Log(torrent.Name)
}
}
func TestSmartSearch(t *testing.T) {
anilistLimiter := limiter.NewAnilistLimiter()
anilistClient := anilist.TestGetMockAnilistClient()
logger := util.NewLogger()
anilistPlatform := anilist_platform.NewAnilistPlatform(anilistClient, logger)
nyaaProvider := NewProvider(util.NewLogger(), categoryAnime)
tests := []struct {
name string
mId int
batch bool
episodeNumber int
absoluteOffset int
resolution string
scrapeMagnet bool
}{
{
name: "Bungou Stray Dogs 5th Season Episode 11",
mId: 163263,
batch: false,
episodeNumber: 11,
absoluteOffset: 45,
resolution: "1080",
scrapeMagnet: true,
},
{
name: "SPY×FAMILY Season 1 Part 2",
mId: 142838,
batch: false,
episodeNumber: 12,
absoluteOffset: 12,
resolution: "1080",
scrapeMagnet: false,
},
{
name: "Jujutsu Kaisen Season 2",
mId: 145064,
batch: false,
episodeNumber: 2,
absoluteOffset: 24,
resolution: "1080",
scrapeMagnet: false,
},
{
name: "Violet Evergarden The Movie",
mId: 103047,
batch: true,
episodeNumber: 1,
absoluteOffset: 0,
resolution: "720",
scrapeMagnet: false,
},
{
name: "Sousou no Frieren",
mId: 154587,
batch: false,
episodeNumber: 10,
absoluteOffset: 0,
resolution: "1080",
scrapeMagnet: false,
},
{
name: "Tokubetsu-hen Hibike! Euphonium: Ensemble",
mId: 150429,
batch: false,
episodeNumber: 1,
absoluteOffset: 0,
resolution: "1080",
scrapeMagnet: false,
},
}
for _, tt := range tests {
anilistLimiter.Wait()
t.Run(tt.name, func(t *testing.T) {
media, err := anilistPlatform.GetAnime(t.Context(), tt.mId)
require.NoError(t, err)
require.NotNil(t, media)
queryMedia := hibiketorrent.Media{
ID: media.GetID(),
IDMal: media.GetIDMal(),
Status: string(*media.GetStatus()),
Format: string(*media.GetFormat()),
EnglishTitle: media.GetTitle().GetEnglish(),
RomajiTitle: media.GetRomajiTitleSafe(),
EpisodeCount: media.GetTotalEpisodeCount(),
AbsoluteSeasonOffset: tt.absoluteOffset,
Synonyms: media.GetSynonymsContainingSeason(),
IsAdult: *media.GetIsAdult(),
StartDate: &hibiketorrent.FuzzyDate{
Year: *media.GetStartDate().GetYear(),
Month: media.GetStartDate().GetMonth(),
Day: media.GetStartDate().GetDay(),
},
}
torrents, err := nyaaProvider.SmartSearch(hibiketorrent.AnimeSmartSearchOptions{
Media: queryMedia,
Query: "",
Batch: tt.batch,
EpisodeNumber: tt.episodeNumber,
Resolution: tt.resolution,
AnidbAID: 0, // Not supported
AnidbEID: 0, // Not supported
BestReleases: false, // Not supported
})
require.NoError(t, err, "error searching nyaa")
for _, torrent := range torrents {
scrapedMagnet := ""
if tt.scrapeMagnet {
magn, err := nyaaProvider.GetTorrentMagnetLink(torrent)
if err == nil {
scrapedMagnet = magn
}
}
t.Log(torrent.Name)
t.Logf("\tMagnet: %s", torrent.MagnetLink)
if scrapedMagnet != "" {
t.Logf("\tMagnet (Scraped): %s", scrapedMagnet)
}
t.Logf("\tEpisodeNumber: %d", torrent.EpisodeNumber)
t.Logf("\tResolution: %s", torrent.Resolution)
t.Logf("\tIsBatch: %v", torrent.IsBatch)
t.Logf("\tConfirmed: %v", torrent.Confirmed)
}
})
}
}

View File

@@ -0,0 +1,159 @@
package nyaa
import (
"errors"
"github.com/gocolly/colly"
"regexp"
"strconv"
"strings"
)
func TorrentFiles(viewURL string) ([]string, error) {
var folders []string
var files []string
c := colly.NewCollector()
c.OnHTML(".folder", func(e *colly.HTMLElement) {
folders = append(folders, e.Text)
})
c.OnHTML(".torrent-file-list", func(e *colly.HTMLElement) {
files = append(files, e.ChildText("li"))
})
var e error
c.OnError(func(r *colly.Response, err error) {
e = err
})
if e != nil {
return nil, e
}
c.Visit(viewURL)
if len(folders) == 0 {
return files, nil
}
return folders, nil
}
func TorrentMagnet(viewURL string) (string, error) {
var magnetLink string
c := colly.NewCollector()
c.OnHTML("a.card-footer-item", func(e *colly.HTMLElement) {
magnetLink = e.Attr("href")
})
var e error
c.OnError(func(r *colly.Response, err error) {
e = err
})
if e != nil {
return "", e
}
c.Visit(viewURL)
if magnetLink == "" {
return "", errors.New("magnet link not found")
}
return magnetLink, nil
}
func TorrentInfo(viewURL string) (title string, seeders int, leechers int, completed int, formattedSize string, infoHash string, magnetLink string, err error) {
c := colly.NewCollector()
c.OnHTML("a.card-footer-item", func(e *colly.HTMLElement) {
magnetLink = e.Attr("href")
})
c.OnHTML(".panel-title", func(e *colly.HTMLElement) {
if title == "" {
title = strings.TrimSpace(e.Text)
}
})
// Find and extract information from the specified div elements
c.OnHTML(".panel-body", func(e *colly.HTMLElement) {
if seeders == 0 {
// Extract seeders
e.ForEach("div:contains('Seeders:') span", func(_ int, el *colly.HTMLElement) {
if el.Attr("style") == "color: green;" {
seeders, _ = strconv.Atoi(el.Text)
}
})
}
if leechers == 0 {
// Extract leechers
e.ForEach("div:contains('Leechers:') span", func(_ int, el *colly.HTMLElement) {
if el.Attr("style") == "color: red;" {
leechers, _ = strconv.Atoi(el.Text)
}
})
}
if completed == 0 {
// Extract completed
e.ForEach("div:contains('Completed:')", func(_ int, el *colly.HTMLElement) {
completed, _ = strconv.Atoi(el.DOM.Parent().Find("div").Next().Next().Next().Text())
})
}
if formattedSize == "" {
// Extract completed
e.ForEach("div:contains('File size:')", func(_ int, el *colly.HTMLElement) {
text := el.DOM.Parent().ChildrenFiltered("div:nth-child(2)").Text()
if !strings.Contains(text, "\t") {
formattedSize = text
}
})
}
if infoHash == "" {
// Extract info hash
e.ForEach("div:contains('Info hash:') kbd", func(_ int, el *colly.HTMLElement) {
infoHash = el.Text
})
}
})
var e error
c.OnError(func(r *colly.Response, err error) {
e = err
})
if e != nil {
err = e
return
}
_ = c.Visit(viewURL)
if magnetLink == "" {
err = errors.New("magnet link not found")
return
}
return
}
func TorrentHash(viewURL string) (string, error) {
magnet, err := TorrentMagnet(viewURL)
if err != nil {
return "", err
}
re := regexp.MustCompile(`magnet:\?xt=urn:btih:([^&]+)`)
match := re.FindStringSubmatch(magnet)
if len(match) > 1 {
return match[1], nil
}
return "", errors.New("could not extract hash")
}

View File

@@ -0,0 +1,54 @@
package nyaa
import (
"seanime/internal/util"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert"
)
func TestTorrentFiles(t *testing.T) {
files, err := TorrentFiles(util.Decode("aHR0cHM6Ly9ueWFhLnNpL3ZpZXcvMTU0MjA1Nw==")) // durarara complete series
assert.NoError(t, err)
t.Log(spew.Sdump(files))
assert.NotEmpty(t, files)
}
func TestTorrentMagnet(t *testing.T) {
magnet, err := TorrentMagnet(util.Decode("aHR0cHM6Ly9ueWFhLnNpL3ZpZXcvMTg4Njg4Ng=="))
assert.NoError(t, err)
t.Log(magnet)
assert.NotEmpty(t, magnet)
}
func TestTorrentInfo(t *testing.T) {
title, a, b, c, fs, d, e, err := TorrentInfo(util.Decode("aHR0cHM6Ly9ueWFhLnNpL3ZpZXcvMTcyNzkyMg=="))
assert.NoError(t, err)
t.Logf("Title: %s\n", title)
t.Logf("Seeders: %d\n", a)
t.Logf("Leechers: %d\n", b)
t.Logf("Downloads: %d\n", c)
t.Logf("Formatted Size: %s\n", fs)
t.Logf("Info Hash: %s\n", d)
t.Logf("Download link: %s\n", e)
}
func TestTorrentHash(t *testing.T) {
hash, err := TorrentHash(util.Decode("aHR0cHM6Ly9ueWFhLnNpL3ZpZXcvMTc0MTY5MQ=="))
assert.NoError(t, err)
t.Log(hash)
assert.NotEmpty(t, hash)
}

View File

@@ -0,0 +1,133 @@
package nyaa
import (
"seanime/internal/extension"
hibiketorrent "seanime/internal/extension/hibike/torrent"
"sync"
"github.com/mmcdole/gofeed"
"github.com/rs/zerolog"
)
const (
SukebeiProviderName = "nyaa-sukebei"
)
type SukebeiProvider struct {
logger *zerolog.Logger
baseUrl string
}
func NewSukebeiProvider(logger *zerolog.Logger) hibiketorrent.AnimeProvider {
return &SukebeiProvider{
logger: logger,
}
}
func (n *SukebeiProvider) SetSavedUserConfig(config extension.SavedUserConfig) {
n.baseUrl, _ = config.Values["apiUrl"]
}
func (n *SukebeiProvider) GetSettings() hibiketorrent.AnimeProviderSettings {
return hibiketorrent.AnimeProviderSettings{
Type: hibiketorrent.AnimeProviderTypeSpecial,
CanSmartSearch: false,
SupportsAdult: true,
}
}
func (n *SukebeiProvider) GetLatest() (ret []*hibiketorrent.AnimeTorrent, err error) {
fp := gofeed.NewParser()
url, err := buildURL(n.baseUrl, BuildURLOptions{
Provider: "sukebei",
Query: "",
Category: "art-anime",
SortBy: "seeders",
Filter: "",
})
if err != nil {
return nil, err
}
// get content
feed, err := fp.ParseURL(url)
if err != nil {
return nil, err
}
// parse content
res := convertRSS(feed)
wg := sync.WaitGroup{}
mu := sync.Mutex{}
for _, torrent := range res {
wg.Add(1)
go func(torrent Torrent) {
defer wg.Done()
mu.Lock()
ret = append(ret, torrent.toAnimeTorrent(SukebeiProviderName))
mu.Unlock()
}(torrent)
}
wg.Wait()
return
}
func (n *SukebeiProvider) Search(opts hibiketorrent.AnimeSearchOptions) (ret []*hibiketorrent.AnimeTorrent, err error) {
fp := gofeed.NewParser()
n.logger.Trace().Str("query", opts.Query).Msg("nyaa: Search query")
url, err := buildURL(n.baseUrl, BuildURLOptions{
Provider: "sukebei",
Query: opts.Query,
Category: "art-anime",
SortBy: "seeders",
Filter: "",
})
if err != nil {
return nil, err
}
// get content
feed, err := fp.ParseURL(url)
if err != nil {
return nil, err
}
// parse content
res := convertRSS(feed)
wg := sync.WaitGroup{}
mu := sync.Mutex{}
for _, torrent := range res {
wg.Add(1)
go func(torrent Torrent) {
defer wg.Done()
mu.Lock()
ret = append(ret, torrent.toAnimeTorrent(SukebeiProviderName))
mu.Unlock()
}(torrent)
}
wg.Wait()
return
}
func (n *SukebeiProvider) SmartSearch(opts hibiketorrent.AnimeSmartSearchOptions) (ret []*hibiketorrent.AnimeTorrent, err error) {
return
}
func (n *SukebeiProvider) GetTorrentInfoHash(torrent *hibiketorrent.AnimeTorrent) (string, error) {
return TorrentHash(torrent.Link)
}
func (n *SukebeiProvider) GetTorrentMagnetLink(torrent *hibiketorrent.AnimeTorrent) (string, error) {
return TorrentMagnet(torrent.Link)
}