mirror of
https://github.com/5rahim/seanime
synced 2026-04-18 22:24:55 +02:00
fix(autodownloader): duplicate downloads with debrid
fix(autoselect): scoring and comparisons
This commit is contained in:
@@ -179,7 +179,7 @@ func (a *AllDebrid) doQuery(method, endpoint string, body io.Reader, contentType
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
a.logger.Debug().Str("method", method).Str("url", u.String()).Msg("alldebrid: doQuery")
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
@@ -234,22 +234,34 @@ func (a *AllDebrid) doQueryCtx(ctx context.Context, method, endpoint string, bod
|
||||
func (a *AllDebrid) AddTorrent(opts debrid.AddTorrentOptions) (string, error) {
|
||||
a.logger.Debug().Msgf("alldebrid: AddTorrent called with: %s", opts.MagnetLink)
|
||||
|
||||
if opts.InfoHash != "" {
|
||||
torrents, err := a.GetTorrents()
|
||||
if err == nil {
|
||||
for _, torrent := range torrents {
|
||||
if strings.EqualFold(torrent.Hash, opts.InfoHash) {
|
||||
a.logger.Debug().Str("torrentId", torrent.ID).Msg("alldebrid: Torrent already added")
|
||||
return torrent.ID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(opts.MagnetLink, "http") {
|
||||
a.logger.Debug().Msg("alldebrid: detected http link, using addTorrentFile")
|
||||
return a.addTorrentFile(opts.MagnetLink)
|
||||
}
|
||||
|
||||
// Endpoint: /magnet/upload
|
||||
|
||||
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
|
||||
|
||||
err := writer.WriteField("magnets[]", opts.MagnetLink)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
|
||||
resp, err := a.doQuery("POST", "/magnet/upload", &body, writer.FormDataContentType())
|
||||
if err != nil {
|
||||
a.logger.Error().Err(err).Msgf("alldebrid: AddTorrent failed. URL: %s/magnet/upload", a.baseUrl)
|
||||
@@ -261,7 +273,7 @@ func (a *AllDebrid) AddTorrent(opts debrid.AddTorrentOptions) (string, error) {
|
||||
if err := json.Unmarshal(b, &data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
||||
if len(data.Magnets) == 0 {
|
||||
return "", fmt.Errorf("no magnet added")
|
||||
}
|
||||
@@ -269,7 +281,7 @@ func (a *AllDebrid) AddTorrent(opts debrid.AddTorrentOptions) (string, error) {
|
||||
if data.Magnets[0].Error != nil {
|
||||
return "", fmt.Errorf("api error: %s", data.Magnets[0].Error.Message)
|
||||
}
|
||||
|
||||
|
||||
return strconv.FormatInt(data.Magnets[0].ID, 10), nil
|
||||
}
|
||||
|
||||
@@ -319,7 +331,7 @@ func (a *AllDebrid) addTorrentFile(urlStr string) (string, error) {
|
||||
// Prepare upload
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
|
||||
|
||||
part, err := writer.CreateFormFile("files[]", "torrent.torrent")
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -353,12 +365,12 @@ func (a *AllDebrid) addTorrentFile(urlStr string) (string, error) {
|
||||
}
|
||||
|
||||
func (a *AllDebrid) GetTorrentStreamUrl(ctx context.Context, opts debrid.StreamTorrentOptions, itemCh chan debrid.TorrentItem) (streamUrl string, err error) {
|
||||
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
|
||||
|
||||
go func(ctx context.Context) {
|
||||
defer close(doneCh)
|
||||
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -372,9 +384,9 @@ func (a *AllDebrid) GetTorrentStreamUrl(ctx context.Context, opts debrid.StreamT
|
||||
a.logger.Error().Err(sErr).Msg("alldebrid: Failed to get torrent status")
|
||||
continue // Retry
|
||||
}
|
||||
|
||||
|
||||
itemCh <- *tInfo
|
||||
|
||||
|
||||
if tInfo.IsReady {
|
||||
// Get download link
|
||||
// We need to find the link that matches the file selected
|
||||
@@ -382,24 +394,24 @@ func (a *AllDebrid) GetTorrentStreamUrl(ctx context.Context, opts debrid.StreamT
|
||||
// AllDebrid links are usually just a list.
|
||||
// We need 'GetTorrentInfo' which returns files list and match?
|
||||
// Or 'GetTorrent' logic.
|
||||
|
||||
|
||||
// Let's call GetTorrentDownloadUrl
|
||||
url, dErr := a.GetTorrentDownloadUrl(debrid.DownloadTorrentOptions{
|
||||
ID: opts.ID,
|
||||
ID: opts.ID,
|
||||
FileId: opts.FileId,
|
||||
})
|
||||
if dErr != nil {
|
||||
a.logger.Error().Err(dErr).Msg("alldebrid: failed to get download url")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
streamUrl = url
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
|
||||
<-doneCh
|
||||
return
|
||||
}
|
||||
@@ -455,11 +467,11 @@ func (a *AllDebrid) GetTorrentDownloadUrl(opts debrid.DownloadTorrentOptions) (s
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
||||
if len(filesResp.Magnets) == 0 {
|
||||
return "", fmt.Errorf("magnet not found")
|
||||
}
|
||||
|
||||
|
||||
info := filesResp.Magnets[0]
|
||||
if info.Error != nil {
|
||||
return "", fmt.Errorf("api error: %s", info.Error.Message)
|
||||
@@ -467,7 +479,7 @@ func (a *AllDebrid) GetTorrentDownloadUrl(opts debrid.DownloadTorrentOptions) (s
|
||||
|
||||
// Flatten the hierarchical file tree
|
||||
flatFiles := flattenFileTree(info.Files, "")
|
||||
|
||||
|
||||
if len(flatFiles) == 0 {
|
||||
return "", fmt.Errorf("no files found in torrent")
|
||||
}
|
||||
@@ -478,11 +490,11 @@ func (a *AllDebrid) GetTorrentDownloadUrl(opts debrid.DownloadTorrentOptions) (s
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid file id: %s", opts.FileId)
|
||||
}
|
||||
|
||||
|
||||
if idx < 0 || idx >= len(flatFiles) {
|
||||
return "", fmt.Errorf("file index out of range")
|
||||
}
|
||||
|
||||
|
||||
// Unlock/Unrestrict the link
|
||||
return a.unlockLink(flatFiles[idx].Link)
|
||||
}
|
||||
@@ -495,7 +507,7 @@ func (a *AllDebrid) GetTorrentDownloadUrl(opts debrid.DownloadTorrentOptions) (s
|
||||
a.logger.Error().Err(err).Str("fileName", file.Name).Msg("alldebrid: Failed to unlock link for file")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
downloadUrls = append(downloadUrls, unlockedUrl)
|
||||
}
|
||||
|
||||
@@ -506,8 +518,6 @@ func (a *AllDebrid) GetTorrentDownloadUrl(opts debrid.DownloadTorrentOptions) (s
|
||||
return strings.Join(downloadUrls, ","), nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (a *AllDebrid) GetInstantAvailability(hashes []string) map[string]debrid.TorrentItemInstantAvailability {
|
||||
// AllDebrid does not have a dedicated instant availability endpoint that checks for cached torrents without adding them.
|
||||
// We return an empty map to indicate no instant availability check is performed.
|
||||
@@ -524,35 +534,35 @@ func (a *AllDebrid) GetTorrent(id string) (*debrid.TorrentItem, error) {
|
||||
|
||||
func (a *AllDebrid) GetTorrentInfo(opts debrid.GetTorrentInfoOptions) (*debrid.TorrentInfo, error) {
|
||||
// Similar to RealDebrid approach: Add -> Get Info -> Delete
|
||||
|
||||
|
||||
if opts.MagnetLink == "" {
|
||||
return nil, fmt.Errorf("magnet link required")
|
||||
}
|
||||
|
||||
|
||||
id, err := a.AddTorrent(debrid.AddTorrentOptions{MagnetLink: opts.MagnetLink})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add torrent for info: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// Fetch info
|
||||
status, err := a.getTorrent(id)
|
||||
if err != nil {
|
||||
a.DeleteTorrent(id)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
// Get files to list them
|
||||
filesResp, err := a.getTorrentFiles(id)
|
||||
if err != nil {
|
||||
a.DeleteTorrent(id)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
if len(filesResp.Magnets) == 0 {
|
||||
a.DeleteTorrent(id)
|
||||
return nil, fmt.Errorf("magnet files not found")
|
||||
}
|
||||
|
||||
|
||||
filesInfo := filesResp.Magnets[0]
|
||||
|
||||
// Create info
|
||||
@@ -562,14 +572,14 @@ func (a *AllDebrid) GetTorrentInfo(opts debrid.GetTorrentInfoOptions) (*debrid.T
|
||||
Hash: status.Hash,
|
||||
Size: status.Size,
|
||||
}
|
||||
|
||||
|
||||
if filesInfo.Files != nil {
|
||||
for i, l := range filesInfo.Files {
|
||||
ret.Files = append(ret.Files, &debrid.TorrentItemFile{
|
||||
ID: strconv.Itoa(i),
|
||||
Index: i,
|
||||
Name: l.Name,
|
||||
Path: l.Name,
|
||||
Path: l.Name,
|
||||
Size: l.Size,
|
||||
})
|
||||
}
|
||||
@@ -577,19 +587,19 @@ func (a *AllDebrid) GetTorrentInfo(opts debrid.GetTorrentInfoOptions) (*debrid.T
|
||||
|
||||
// Delete
|
||||
a.DeleteTorrent(id)
|
||||
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (a *AllDebrid) GetTorrents() ([]*debrid.TorrentItem, error) {
|
||||
endpoint := "/magnet/status"
|
||||
|
||||
|
||||
// v4.1 API requires POST, not GET
|
||||
resp, err := a.doQuery("POST", endpoint, nil, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
var data GetTorrentsResponse
|
||||
b, _ := json.Marshal(resp.Data)
|
||||
json.Unmarshal(b, &data)
|
||||
@@ -598,7 +608,7 @@ func (a *AllDebrid) GetTorrents() ([]*debrid.TorrentItem, error) {
|
||||
for _, m := range data.Magnets {
|
||||
ret = append(ret, toDebridTorrent(&m))
|
||||
}
|
||||
|
||||
|
||||
// Sort by ID desc
|
||||
slices.SortFunc(ret, func(i, j *debrid.TorrentItem) int {
|
||||
return strings.Compare(j.ID, i.ID)
|
||||
@@ -629,48 +639,48 @@ func (a *AllDebrid) DeleteTorrent(id string) error {
|
||||
|
||||
func (a *AllDebrid) getTorrent(id string) (*Torrent, error) {
|
||||
endpoint := "/magnet/status"
|
||||
|
||||
|
||||
var body io.Reader
|
||||
var contentType string
|
||||
|
||||
|
||||
if id != "" {
|
||||
// v4.1 API requires POST with form data, not GET with query params
|
||||
var formBody bytes.Buffer
|
||||
writer := multipart.NewWriter(&formBody)
|
||||
writer.WriteField("id", id)
|
||||
writer.Close()
|
||||
|
||||
|
||||
body = &formBody
|
||||
contentType = writer.FormDataContentType()
|
||||
}
|
||||
|
||||
|
||||
resp, err := a.doQuery("POST", endpoint, body, contentType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
if id != "" {
|
||||
var data GetTorrentResponse
|
||||
b, _ := json.Marshal(resp.Data)
|
||||
json.Unmarshal(b, &data)
|
||||
|
||||
|
||||
if data.Magnets.ID == 0 {
|
||||
a.logger.Error().Any("data", data).Msg("alldebrid: getTorrent - magnet not found in response")
|
||||
return nil, fmt.Errorf("magnet not found")
|
||||
}
|
||||
return &data.Magnets, nil
|
||||
}
|
||||
|
||||
|
||||
// This branch should mostly not be used by this helper as it's typically called with ID
|
||||
// But if it is...
|
||||
var data GetTorrentsResponse
|
||||
b, _ := json.Marshal(resp.Data)
|
||||
json.Unmarshal(b, &data)
|
||||
|
||||
|
||||
if len(data.Magnets) == 0 {
|
||||
return nil, fmt.Errorf("magnet not found")
|
||||
}
|
||||
|
||||
|
||||
return &data.Magnets[0], nil
|
||||
}
|
||||
|
||||
@@ -687,13 +697,13 @@ func (a *AllDebrid) getTorrentFiles(id string) (*GetTorrentFilesResponse, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
var data GetTorrentFilesResponse
|
||||
b, _ := json.Marshal(resp.Data)
|
||||
if err := json.Unmarshal(b, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
@@ -710,23 +720,23 @@ func (a *AllDebrid) unlockLink(link string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
||||
var data UnrestrictLinkResponse
|
||||
b, _ := json.Marshal(resp.Data)
|
||||
json.Unmarshal(b, &data)
|
||||
|
||||
|
||||
return data.Link, nil
|
||||
}
|
||||
|
||||
func toDebridTorrent(t *Torrent) *debrid.TorrentItem {
|
||||
status := toDebridTorrentStatus(t)
|
||||
|
||||
|
||||
// Convert Unix timestamp to RFC3339 format
|
||||
addedAt := ""
|
||||
if t.UploadDate > 0 {
|
||||
addedAt = time.Unix(t.UploadDate, 0).Format(time.RFC3339)
|
||||
}
|
||||
|
||||
|
||||
// Calculate completion percentage
|
||||
completionPercentage := 0
|
||||
if t.Size > 0 && t.Downloaded > 0 {
|
||||
@@ -742,7 +752,7 @@ func toDebridTorrent(t *Torrent) *debrid.TorrentItem {
|
||||
Size: t.Size,
|
||||
FormattedSize: util.Bytes(uint64(t.Size)),
|
||||
CompletionPercentage: completionPercentage,
|
||||
ETA: "",
|
||||
ETA: "",
|
||||
Status: status,
|
||||
AddedAt: addedAt,
|
||||
Speed: util.ToHumanReadableSpeed(int(t.DownloadSpeed)),
|
||||
@@ -767,4 +777,4 @@ func toDebridTorrentStatus(t *Torrent) debrid.TorrentItemStatus {
|
||||
}
|
||||
return debrid.TorrentItemStatusOther
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ func (t *RealDebrid) AddTorrent(opts debrid.AddTorrentOptions) (string, error) {
|
||||
torrents, err := t.getTorrents(false)
|
||||
if err == nil {
|
||||
for _, torrent := range torrents {
|
||||
if torrent.Hash == opts.InfoHash {
|
||||
if strings.EqualFold(torrent.Hash, opts.InfoHash) {
|
||||
t.logger.Debug().Str("torrentId", torrent.ID).Msg("realdebrid: Torrent already added")
|
||||
torrentId = torrent.ID
|
||||
break
|
||||
|
||||
@@ -238,7 +238,7 @@ func (t *TorBox) AddTorrent(opts debrid.AddTorrentOptions) (string, error) {
|
||||
torrents, err := t.getTorrents()
|
||||
if err == nil {
|
||||
for _, torrent := range torrents {
|
||||
if torrent.Hash == opts.InfoHash {
|
||||
if strings.EqualFold(torrent.Hash, opts.InfoHash) {
|
||||
return strconv.Itoa(torrent.ID), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ func (h *Handler) HandleDebridAddTorrents(c echo.Context) error {
|
||||
// Add the torrent to the debrid service
|
||||
_, err = h.App.DebridClientRepository.AddAndQueueTorrent(debrid.AddTorrentOptions{
|
||||
MagnetLink: magnet,
|
||||
InfoHash: torrent.InfoHash,
|
||||
SelectFileId: "all",
|
||||
}, b.Destination, b.Media.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -323,11 +323,38 @@ func (ad *AutoDownloader) checkForNewEpisodes(ctx context.Context, isSimulation
|
||||
|
||||
// runData holds all data needed for checking new episodes
|
||||
type runData struct {
|
||||
rules []*anime.AutoDownloaderRule
|
||||
profiles []*anime.AutoDownloaderProfile
|
||||
localFileWrapper *anime.LocalFileWrapper
|
||||
torrents []*NormalizedTorrent
|
||||
existingTorrents []*torrent_client.Torrent
|
||||
rules []*anime.AutoDownloaderRule
|
||||
profiles []*anime.AutoDownloaderProfile
|
||||
localFileWrapper *anime.LocalFileWrapper
|
||||
torrents []*NormalizedTorrent
|
||||
existingTorrentHashes map[string]struct{}
|
||||
}
|
||||
|
||||
func normalizeTorrentHash(hash string) string {
|
||||
return strings.ToLower(strings.TrimSpace(hash))
|
||||
}
|
||||
|
||||
func addTorrentHash(hashSet map[string]struct{}, hash string) {
|
||||
normalizedHash := normalizeTorrentHash(hash)
|
||||
if normalizedHash == "" {
|
||||
return
|
||||
}
|
||||
|
||||
hashSet[normalizedHash] = struct{}{}
|
||||
}
|
||||
|
||||
func buildExistingTorrentHashes(existingTorrents []*torrent_client.Torrent, existingDebridTorrents []*debrid.TorrentItem) map[string]struct{} {
|
||||
hashes := make(map[string]struct{}, len(existingTorrents)+len(existingDebridTorrents))
|
||||
|
||||
for _, item := range existingTorrents {
|
||||
addTorrentHash(hashes, item.Hash)
|
||||
}
|
||||
|
||||
for _, item := range existingDebridTorrents {
|
||||
addTorrentHash(hashes, item.Hash)
|
||||
}
|
||||
|
||||
return hashes
|
||||
}
|
||||
|
||||
// fetchRunData fetches all data needed for checking new episodes
|
||||
@@ -402,12 +429,23 @@ func (ad *AutoDownloader) fetchRunData(ctx context.Context, ruleIDs ...uint) (*r
|
||||
existingTorrents, _ = ad.torrentClientRepository.GetList(&torrent_client.GetListOptions{})
|
||||
}
|
||||
|
||||
var existingDebridTorrents []*debrid.TorrentItem
|
||||
if ad.settings.UseDebrid && ad.debridClientRepository != nil && ad.debridClientRepository.HasProvider() {
|
||||
provider, err := ad.debridClientRepository.GetProvider()
|
||||
if err == nil {
|
||||
existingDebridTorrents, err = provider.GetTorrents()
|
||||
if err != nil {
|
||||
ad.logger.Debug().Err(err).Msg("autodownloader: Failed to get debrid torrents for duplicate check")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &runData{
|
||||
rules: rules,
|
||||
profiles: profiles,
|
||||
localFileWrapper: lfWrapper,
|
||||
torrents: torrents,
|
||||
existingTorrents: existingTorrents,
|
||||
rules: rules,
|
||||
profiles: profiles,
|
||||
localFileWrapper: lfWrapper,
|
||||
torrents: torrents,
|
||||
existingTorrentHashes: buildExistingTorrentHashes(existingTorrents, existingDebridTorrents),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -444,7 +482,7 @@ func (ad *AutoDownloader) groupTorrentCandidates(data *runData) map[uint]map[int
|
||||
// Process each torrent
|
||||
for _, t := range data.torrents {
|
||||
// Skip if already exists
|
||||
if ad.isTorrentAlreadyDownloaded(t, data.existingTorrents) {
|
||||
if ad.isTorrentAlreadyDownloaded(t, data.existingTorrentHashes) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -495,14 +533,10 @@ func (ad *AutoDownloader) getRuleProfiles(rule *anime.AutoDownloaderRule, profil
|
||||
return ruleProfiles
|
||||
}
|
||||
|
||||
// isTorrentAlreadyDownloaded checks if a torrent already exists in the client
|
||||
func (ad *AutoDownloader) isTorrentAlreadyDownloaded(t *NormalizedTorrent, existingTorrents []*torrent_client.Torrent) bool {
|
||||
for _, et := range existingTorrents {
|
||||
if et.Hash == t.InfoHash {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// isTorrentAlreadyDownloaded checks if a torrent already exists in the client or debrid service.
|
||||
func (ad *AutoDownloader) isTorrentAlreadyDownloaded(t *NormalizedTorrent, existingTorrentHashes map[string]struct{}) bool {
|
||||
_, exists := existingTorrentHashes[normalizeTorrentHash(t.InfoHash)]
|
||||
return exists
|
||||
}
|
||||
|
||||
// isEpisodeAlreadyHandled checks if an episode is already in the library or queue but not delayed
|
||||
@@ -1097,6 +1131,7 @@ downloadScope:
|
||||
// Add the torrent to the debrid provider and queue it
|
||||
_, err := ad.debridClientRepository.AddAndQueueTorrent(debrid.AddTorrentOptions{
|
||||
MagnetLink: magnet,
|
||||
InfoHash: t.InfoHash,
|
||||
SelectFileId: "all", // RD-only, select all files
|
||||
}, rule.Destination, rule.MediaId)
|
||||
if err != nil {
|
||||
@@ -1115,6 +1150,7 @@ downloadScope:
|
||||
// Add the torrent to the debrid provider
|
||||
_, err = debridProvider.AddTorrent(debrid.AddTorrentOptions{
|
||||
MagnetLink: magnet,
|
||||
InfoHash: t.InfoHash,
|
||||
SelectFileId: "all", // RD-only, select all files
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/database/db_bridge"
|
||||
"seanime/internal/database/models"
|
||||
"seanime/internal/debrid/debrid"
|
||||
hibiketorrent "seanime/internal/extension/hibike/torrent"
|
||||
"seanime/internal/library/anime"
|
||||
"seanime/internal/test_utils"
|
||||
@@ -233,14 +234,26 @@ func TestGetRuleProfiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildExistingTorrentHashes(t *testing.T) {
|
||||
hashes := buildExistingTorrentHashes(
|
||||
[]*torrent_client.Torrent{{Hash: " hash1 "}, {Hash: "HASH2"}},
|
||||
[]*debrid.TorrentItem{{Hash: "hash3"}, {Hash: "HaSh4"}, {Hash: ""}},
|
||||
)
|
||||
|
||||
assert.Len(t, hashes, 4)
|
||||
assert.Contains(t, hashes, "hash1")
|
||||
assert.Contains(t, hashes, "hash2")
|
||||
assert.Contains(t, hashes, "hash3")
|
||||
assert.Contains(t, hashes, "hash4")
|
||||
}
|
||||
|
||||
func TestIsTorrentAlreadyDownloaded(t *testing.T) {
|
||||
ad := &AutoDownloader{}
|
||||
|
||||
existingTorrents := []*torrent_client.Torrent{
|
||||
{Hash: "hash1"},
|
||||
{Hash: "hash2"},
|
||||
{Hash: "hash3"},
|
||||
}
|
||||
existingTorrentHashes := buildExistingTorrentHashes(
|
||||
[]*torrent_client.Torrent{{Hash: "hash1"}, {Hash: "hash2"}},
|
||||
[]*debrid.TorrentItem{{Hash: "hash3"}},
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -250,7 +263,14 @@ func TestIsTorrentAlreadyDownloaded(t *testing.T) {
|
||||
{
|
||||
name: "torrent exists",
|
||||
torrent: &NormalizedTorrent{
|
||||
AnimeTorrent: &hibiketorrent.AnimeTorrent{InfoHash: "hash2"},
|
||||
AnimeTorrent: &hibiketorrent.AnimeTorrent{InfoHash: " HASH2 "},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "torrent exists in debrid hashes",
|
||||
torrent: &NormalizedTorrent{
|
||||
AnimeTorrent: &hibiketorrent.AnimeTorrent{InfoHash: "hash3"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
@@ -265,7 +285,7 @@ func TestIsTorrentAlreadyDownloaded(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ad.isTorrentAlreadyDownloaded(tt.torrent, existingTorrents)
|
||||
result := ad.isTorrentAlreadyDownloaded(tt.torrent, existingTorrentHashes)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,80 +1 @@
|
||||
package torrent_client
|
||||
|
||||
//func TestSmartSelect(t *testing.T) {
|
||||
// t.Skip("Refactor test")
|
||||
// test_utils.InitTestProvider(t, test_utils.TorrentClient())
|
||||
//
|
||||
// _ = t.TempDir()
|
||||
//
|
||||
// anilistClient := anilist.TestGetMockAnilistClient()
|
||||
// _ = anilist_platform.NewAnilistPlatform(anilistClient, util.NewLogger())
|
||||
//
|
||||
// // get repo
|
||||
//
|
||||
// tests := []struct {
|
||||
// name string
|
||||
// mediaId int
|
||||
// url string
|
||||
// selectedEpisodes []int
|
||||
// client string
|
||||
// }{
|
||||
// {
|
||||
// name: "Kakegurui xx (Season 2)",
|
||||
// mediaId: 100876,
|
||||
// url: "https://nyaa.si/view/1553978", // kakegurui season 1 + season 2
|
||||
// selectedEpisodes: []int{10, 11, 12}, // should select 10, 11, 12 in season 2
|
||||
// client: QbittorrentClient,
|
||||
// },
|
||||
// {
|
||||
// name: "Spy x Family",
|
||||
// mediaId: 140960,
|
||||
// url: "https://nyaa.si/view/1661695", // spy x family (01-25)
|
||||
// selectedEpisodes: []int{10, 11, 12}, // should select 10, 11, 12
|
||||
// client: QbittorrentClient,
|
||||
// },
|
||||
// {
|
||||
// name: "Spy x Family Part 2",
|
||||
// mediaId: 142838,
|
||||
// url: "https://nyaa.si/view/1661695", // spy x family (01-25)
|
||||
// selectedEpisodes: []int{10, 11, 12, 13}, // should select 22, 23, 24, 25
|
||||
// client: QbittorrentClient,
|
||||
// },
|
||||
// {
|
||||
// name: "Kakegurui xx (Season 2)",
|
||||
// mediaId: 100876,
|
||||
// url: "https://nyaa.si/view/1553978", // kakegurui season 1 + season 2
|
||||
// selectedEpisodes: []int{10, 11, 12}, // should select 10, 11, 12 in season 2
|
||||
// client: TransmissionClient,
|
||||
// },
|
||||
// {
|
||||
// name: "Spy x Family",
|
||||
// mediaId: 140960,
|
||||
// url: "https://nyaa.si/view/1661695", // spy x family (01-25)
|
||||
// selectedEpisodes: []int{10, 11, 12}, // should select 10, 11, 12
|
||||
// client: TransmissionClient,
|
||||
// },
|
||||
// {
|
||||
// name: "Spy x Family Part 2",
|
||||
// mediaId: 142838,
|
||||
// url: "https://nyaa.si/view/1661695", // spy x family (01-25)
|
||||
// selectedEpisodes: []int{10, 11, 12, 13}, // should select 22, 23, 24, 25
|
||||
// client: TransmissionClient,
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// for _, tt := range tests {
|
||||
//
|
||||
// t.Run(tt.name, func(t *testing.T) {
|
||||
//
|
||||
// repo := getTestRepo(t, tt.client)
|
||||
//
|
||||
// ok := repo.Start()
|
||||
// if !assert.True(t, ok) {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// })
|
||||
//
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
@@ -2,13 +2,14 @@ package transmission
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hekmon/transmissionrpc/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hekmon/transmissionrpc/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//func TestGetActiveTorrents(t *testing.T) {
|
||||
@@ -44,14 +45,14 @@ func TestGetFiles(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "[EMBER] Demon Slayer (2023) (Season 3)",
|
||||
url: "https://animetosho.org/view/ember-demon-slayer-2023-season-3-bdrip-1080p.n1778316",
|
||||
url: "",
|
||||
magnet: "",
|
||||
mediaId: 145139,
|
||||
expectedNbFiles: 11,
|
||||
},
|
||||
{
|
||||
name: "[Tenrai-Sensei] Kakegurui (Season 1-2 + OVAs)",
|
||||
url: "https://nyaa.si/view/1553978",
|
||||
url: "",
|
||||
magnet: "",
|
||||
mediaId: 98314,
|
||||
expectedNbFiles: 27,
|
||||
|
||||
@@ -101,6 +101,14 @@ func TestAutoSelect_Filter(t *testing.T) {
|
||||
},
|
||||
expected: []string{t8.Name}, // assuming habari detects Web-DL properly or fallback check works
|
||||
},
|
||||
{
|
||||
name: "Source token should not match inside another word",
|
||||
profile: &anime.AutoSelectProfile{
|
||||
RequireSource: true,
|
||||
PreferredSources: []string{"CR"},
|
||||
},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "Dual audio only",
|
||||
profile: &anime.AutoSelectProfile{
|
||||
@@ -243,6 +251,78 @@ func TestAutoSelect_Sort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoSelect_Filter_SourceTokenDoesNotMatchInsideWord(t *testing.T) {
|
||||
s := newTestAutoSelect()
|
||||
|
||||
torrents := []*hibiketorrent.AnimeTorrent{
|
||||
{Name: "Rooster Fighter - 01 A Rooster Among Cranes 1080p WEB-DL.mkv", Seeders: 50},
|
||||
}
|
||||
|
||||
filtered := s.filter(torrents, &anime.AutoSelectProfile{
|
||||
RequireSource: true,
|
||||
PreferredSources: []string{"CR"},
|
||||
})
|
||||
|
||||
assert.Empty(t, filtered)
|
||||
}
|
||||
|
||||
func TestAutoSelect_Sort_OrderedPreferencesBeatSoftBonuses(t *testing.T) {
|
||||
s := newTestAutoSelect()
|
||||
|
||||
preferred := &hibiketorrent.AnimeTorrent{
|
||||
Name: "[ToonsHub] Show - 01 1080p CR WEB-DL AAC2.0 H.264 (Multi-Subs).mkv",
|
||||
Seeders: 50,
|
||||
Provider: "catnoise",
|
||||
}
|
||||
lowerPriorityButBonusHeavy := &hibiketorrent.AnimeTorrent{
|
||||
Name: "Show - 01 1080p DSNP WEB-DL DUAL AAC2.0 H.264-VARYG (Dual-Audio, Multi-Subs).mkv",
|
||||
Seeders: 100,
|
||||
Provider: "catnoise",
|
||||
IsBestRelease: true,
|
||||
}
|
||||
|
||||
torrents := []*hibiketorrent.AnimeTorrent{lowerPriorityButBonusHeavy, preferred}
|
||||
profile := &anime.AutoSelectProfile{
|
||||
Providers: []string{"catnoise"},
|
||||
ReleaseGroups: []string{"ToonsHub", "VARYG"},
|
||||
Resolutions: []string{"1080p"},
|
||||
PreferredCodecs: []string{"AVC, x264, H.264, H264, H 264"},
|
||||
PreferredSources: []string{"CR", "DSNP"},
|
||||
BestReleasePreference: anime.AutoSelectPreferencePrefer,
|
||||
}
|
||||
|
||||
s.sort(torrents, profile)
|
||||
|
||||
assert.Equal(t, []string{preferred.Name, lowerPriorityButBonusHeavy.Name}, []string{torrents[0].Name, torrents[1].Name})
|
||||
}
|
||||
|
||||
func TestAutoSelect_Sort_StrongerPrimaryMatchCanBeatHigherReleaseGroup(t *testing.T) {
|
||||
s := newTestAutoSelect()
|
||||
|
||||
higherReleaseGroupButLowerQuality := &hibiketorrent.AnimeTorrent{
|
||||
Name: "[ToonsHub] Show - 01 720p CR WEB-DL AAC2.0 H.264.mkv",
|
||||
Seeders: 80,
|
||||
Provider: "catnoise",
|
||||
}
|
||||
lowerReleaseGroupButHigherResolution := &hibiketorrent.AnimeTorrent{
|
||||
Name: "Show - 01 1080p DSNP WEB-DL AAC2.0 H.264-VARYG.mkv",
|
||||
Seeders: 40,
|
||||
Provider: "catnoise",
|
||||
}
|
||||
|
||||
torrents := []*hibiketorrent.AnimeTorrent{higherReleaseGroupButLowerQuality, lowerReleaseGroupButHigherResolution}
|
||||
profile := &anime.AutoSelectProfile{
|
||||
ReleaseGroups: []string{"ToonsHub", "VARYG"},
|
||||
Resolutions: []string{"1080p"},
|
||||
PreferredCodecs: []string{"AVC, x264, H.264, H264, H 264"},
|
||||
PreferredSources: []string{"CR", "DSNP"},
|
||||
}
|
||||
|
||||
s.sort(torrents, profile)
|
||||
|
||||
assert.Equal(t, []string{lowerReleaseGroupButHigherResolution.Name, higherReleaseGroupButLowerQuality.Name}, []string{torrents[0].Name, torrents[1].Name})
|
||||
}
|
||||
|
||||
func TestAutoSelect_SmartCachedPrioritization(t *testing.T) {
|
||||
s := newTestAutoSelect()
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
scoreBestReleaseBase = 200
|
||||
scoreResolutionBase = 100
|
||||
scoreResolutionDecay = 10
|
||||
scoreProviderBase = 5
|
||||
@@ -35,6 +34,8 @@ type candidate struct {
|
||||
torrent *hibiketorrent.AnimeTorrent
|
||||
parsed *habari.Metadata
|
||||
lowerName string
|
||||
priority int
|
||||
bonus int
|
||||
score int
|
||||
}
|
||||
|
||||
@@ -152,6 +153,36 @@ func checkPreference(condition bool, preference anime.AutoSelectPreference) bool
|
||||
return true
|
||||
}
|
||||
|
||||
func isTokenChar(char byte) bool {
|
||||
return (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9')
|
||||
}
|
||||
|
||||
func containsBoundedTerm(lowerValue string, term string) bool {
|
||||
lowerTerm := strings.ToLower(strings.TrimSpace(term))
|
||||
if lowerTerm == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
searchFrom := 0
|
||||
for {
|
||||
idx := strings.Index(lowerValue[searchFrom:], lowerTerm)
|
||||
if idx == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
idx += searchFrom
|
||||
end := idx + len(lowerTerm)
|
||||
|
||||
leftBoundary := idx == 0 || !isTokenChar(lowerValue[idx-1])
|
||||
rightBoundary := end == len(lowerValue) || !isTokenChar(lowerValue[end])
|
||||
if leftBoundary && rightBoundary {
|
||||
return true
|
||||
}
|
||||
|
||||
searchFrom = idx + 1
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AutoSelect) filterCandidates(candidates []*candidate, profile *anime.AutoSelectProfile) []*candidate {
|
||||
if profile == nil {
|
||||
return candidates
|
||||
@@ -224,7 +255,7 @@ func (s *AutoSelect) filterCandidates(candidates []*candidate, profile *anime.Au
|
||||
}
|
||||
} else { // Fallback to string matching
|
||||
for _, lang := range preferredLanguages {
|
||||
if len(lang) > 3 && strings.Contains(c.lowerName, strings.ToLower(lang)) {
|
||||
if len(lang) > 3 && containsBoundedTerm(c.lowerName, lang) {
|
||||
foundLang = true
|
||||
break
|
||||
}
|
||||
@@ -258,7 +289,7 @@ func (s *AutoSelect) filterCandidates(candidates []*candidate, profile *anime.Au
|
||||
foundCodec = true
|
||||
break
|
||||
}
|
||||
if strings.Contains(c.lowerName, strings.ToLower(codec)) {
|
||||
if containsBoundedTerm(c.lowerName, codec) {
|
||||
foundCodec = true
|
||||
break
|
||||
}
|
||||
@@ -278,7 +309,7 @@ func (s *AutoSelect) filterCandidates(candidates []*candidate, profile *anime.Au
|
||||
foundSource = true
|
||||
break
|
||||
}
|
||||
if strings.Contains(c.lowerName, strings.ToLower(source)) {
|
||||
if containsBoundedTerm(c.lowerName, source) {
|
||||
foundSource = true
|
||||
break
|
||||
}
|
||||
@@ -306,12 +337,21 @@ func (s *AutoSelect) filterCandidates(candidates []*candidate, profile *anime.Au
|
||||
|
||||
func (s *AutoSelect) sortCandidates(candidates []*candidate, profile *anime.AutoSelectProfile) {
|
||||
for _, c := range candidates {
|
||||
c.score = s.calculateScore(c, profile)
|
||||
c.priority, c.bonus = s.calculateScoreBreakdown(c, profile)
|
||||
c.score = c.priority + c.bonus
|
||||
}
|
||||
|
||||
slices.SortStableFunc(candidates, func(a, b *candidate) int {
|
||||
if a.priority != b.priority {
|
||||
return cmp.Compare(b.priority, a.priority)
|
||||
}
|
||||
|
||||
if a.bonus != b.bonus {
|
||||
return cmp.Compare(b.bonus, a.bonus)
|
||||
}
|
||||
|
||||
if a.score != b.score {
|
||||
return cmp.Compare(b.score, a.score) // Higher score first
|
||||
return cmp.Compare(b.score, a.score)
|
||||
}
|
||||
|
||||
// If the scores are the same, sort by seeders
|
||||
@@ -325,7 +365,7 @@ func (s *AutoSelect) sortCandidates(candidates []*candidate, profile *anime.Auto
|
||||
func (s *AutoSelect) smartCachedPrioritization(
|
||||
torrents []*hibiketorrent.AnimeTorrent,
|
||||
candidates []*candidate,
|
||||
profile *anime.AutoSelectProfile,
|
||||
_ *anime.AutoSelectProfile,
|
||||
postSearchSort func([]*hibiketorrent.AnimeTorrent) []*TorrentWithCacheStatus,
|
||||
) []*hibiketorrent.AnimeTorrent {
|
||||
|
||||
@@ -395,28 +435,23 @@ func (s *AutoSelect) smartCachedPrioritization(
|
||||
}
|
||||
|
||||
func (s *AutoSelect) calculateScore(c *candidate, profile *anime.AutoSelectProfile) int {
|
||||
score := 0
|
||||
priority, bonus := s.calculateScoreBreakdown(c, profile)
|
||||
return priority + bonus
|
||||
}
|
||||
|
||||
func (s *AutoSelect) calculateScoreBreakdown(c *candidate, profile *anime.AutoSelectProfile) (priority int, bonus int) {
|
||||
parsed := c.parsed
|
||||
t := c.torrent
|
||||
|
||||
// Boost by provider order
|
||||
|
||||
if profile == nil {
|
||||
return score
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
//// Awlays best releases as a base line if user doesn't reject them
|
||||
//if (profile.BestReleasePreference != anime.AutoSelectPreferenceAvoid && profile.BestReleasePreference != anime.AutoSelectPreferenceNever) &&
|
||||
// c.torrent.IsBestRelease && (t.Seeders == -1 || t.Seeders > 2) {
|
||||
// // boost only if user isn't looking for low resolutions
|
||||
// score += scoreBestReleaseBase
|
||||
//}
|
||||
|
||||
// Resolution
|
||||
if len(profile.Resolutions) > 0 {
|
||||
for i, res := range profile.Resolutions {
|
||||
if strings.EqualFold(parsed.VideoResolution, res) {
|
||||
score += scoreResolutionBase - (i * scoreResolutionDecay)
|
||||
priority += scoreResolutionBase - (i * scoreResolutionDecay)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -426,7 +461,7 @@ func (s *AutoSelect) calculateScore(c *candidate, profile *anime.AutoSelectProfi
|
||||
if len(profile.Providers) > 0 {
|
||||
for i, provider := range profile.Providers {
|
||||
if strings.EqualFold(t.Provider, provider) {
|
||||
score += scoreProviderBase - (i * scoreProviderDecay)
|
||||
priority += scoreProviderBase - (i * scoreProviderDecay)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -436,7 +471,7 @@ func (s *AutoSelect) calculateScore(c *candidate, profile *anime.AutoSelectProfi
|
||||
if len(profile.ReleaseGroups) > 0 {
|
||||
for i, group := range profile.ReleaseGroups {
|
||||
if strings.EqualFold(parsed.ReleaseGroup, group) {
|
||||
score += scoreReleaseGroupBase - (i * scoreReleaseGroupDecay)
|
||||
priority += scoreReleaseGroupBase - (i * scoreReleaseGroupDecay)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -445,6 +480,7 @@ func (s *AutoSelect) calculateScore(c *candidate, profile *anime.AutoSelectProfi
|
||||
// Codec
|
||||
if len(profile.PreferredCodecs) > 0 {
|
||||
for i, codecs := range profile.PreferredCodecs {
|
||||
matched := false
|
||||
for _, codec := range strings.Split(codecs, ",") {
|
||||
codec = strings.TrimSpace(codec)
|
||||
if slices.ContainsFunc(parsed.VideoTerm, func(vt string) bool {
|
||||
@@ -452,87 +488,102 @@ func (s *AutoSelect) calculateScore(c *candidate, profile *anime.AutoSelectProfi
|
||||
}) || slices.ContainsFunc(parsed.AudioTerm, func(at string) bool {
|
||||
return strings.EqualFold(at, codec)
|
||||
}) {
|
||||
score += scoreCodecBase - (i * scoreCodecDecay)
|
||||
priority += scoreCodecBase - (i * scoreCodecDecay)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
// Fallback check
|
||||
if strings.Contains(c.lowerName, strings.ToLower(codec)) {
|
||||
score += scoreCodecBase - (i * scoreCodecDecay)
|
||||
if containsBoundedTerm(c.lowerName, codec) {
|
||||
priority += scoreCodecBase - (i * scoreCodecDecay)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Source
|
||||
if len(profile.PreferredSources) > 0 {
|
||||
for i, sources := range profile.PreferredSources {
|
||||
matched := false
|
||||
for _, source := range strings.Split(sources, ",") {
|
||||
source = strings.TrimSpace(source)
|
||||
if slices.ContainsFunc(parsed.Source, func(src string) bool {
|
||||
return strings.EqualFold(src, source)
|
||||
}) {
|
||||
score += scoreSourceBase - (i * scoreSourceDecay)
|
||||
priority += scoreSourceBase - (i * scoreSourceDecay)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
if strings.Contains(c.lowerName, strings.ToLower(source)) {
|
||||
score += scoreSourceBase - (i * scoreSourceDecay)
|
||||
if containsBoundedTerm(c.lowerName, source) {
|
||||
priority += scoreSourceBase - (i * scoreSourceDecay)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Language
|
||||
if len(profile.PreferredLanguages) > 0 {
|
||||
for i, languages := range profile.PreferredLanguages {
|
||||
matched := false
|
||||
for _, lang := range strings.Split(languages, ",") {
|
||||
lang = strings.TrimSpace(lang)
|
||||
if slices.ContainsFunc(parsed.Language, func(pl string) bool {
|
||||
return strings.EqualFold(pl, lang)
|
||||
}) {
|
||||
score += scoreLanguageBase - (i * scoreLanguageDecay)
|
||||
}) || containsBoundedTerm(c.lowerName, lang) {
|
||||
priority += scoreLanguageBase - (i * scoreLanguageDecay)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple audio preference (prefer/avoid)
|
||||
isMultiAudio := containsMultiOrDual(parsed.AudioTerm)
|
||||
if profile.MultipleAudioPreference == anime.AutoSelectPreferencePrefer && isMultiAudio {
|
||||
score += scoreMultiAudio
|
||||
bonus += scoreMultiAudio
|
||||
}
|
||||
if profile.MultipleAudioPreference == anime.AutoSelectPreferenceAvoid && isMultiAudio {
|
||||
score -= scoreMultiAudio
|
||||
bonus -= scoreMultiAudio
|
||||
}
|
||||
|
||||
// Multiple subs preference (prefer/avoid)
|
||||
isMultiSubs := containsMultiOrDual(parsed.Subtitles)
|
||||
if profile.MultipleSubsPreference == anime.AutoSelectPreferencePrefer && isMultiSubs {
|
||||
score += scoreMultiSubs
|
||||
bonus += scoreMultiSubs
|
||||
}
|
||||
if profile.MultipleSubsPreference == anime.AutoSelectPreferenceAvoid && isMultiSubs {
|
||||
score -= scoreMultiSubs
|
||||
bonus -= scoreMultiSubs
|
||||
}
|
||||
|
||||
// Batch preference (prefer/avoid)
|
||||
isBatch := t.IsBatch
|
||||
if profile.BatchPreference == anime.AutoSelectPreferencePrefer && isBatch {
|
||||
score += scoreBatch
|
||||
bonus += scoreBatch
|
||||
}
|
||||
if profile.BatchPreference == anime.AutoSelectPreferenceAvoid && isBatch {
|
||||
score -= scoreBatch
|
||||
bonus -= scoreBatch
|
||||
}
|
||||
|
||||
// Best release preference (prefer/avoid)
|
||||
isBestRelease := t.IsBestRelease && (t.Seeders == -1 || t.Seeders > 2)
|
||||
if profile.BestReleasePreference == anime.AutoSelectPreferencePrefer && isBestRelease {
|
||||
score += scoreBestRelease
|
||||
bonus += scoreBestRelease
|
||||
}
|
||||
if profile.BestReleasePreference == anime.AutoSelectPreferenceAvoid && isBestRelease {
|
||||
score -= scoreBestRelease
|
||||
bonus -= scoreBestRelease
|
||||
}
|
||||
|
||||
return score
|
||||
return priority, bonus
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user