initial commit

This commit is contained in:
2025-06-06 16:39:45 +02:00
commit 5c3d781822
13 changed files with 500 additions and 0 deletions

39
access_token.go Normal file
View File

@ -0,0 +1,39 @@
package logic
import (
"io"
"net/http"
"strings"
)
func GetAnilistAccessToken() (string, error) {
if strings.HasPrefix(AnilistAccessToken, "ey") {
return AnilistAccessToken, nil
}
if StoragePath == "" {
return "", ErrAnilistTokenNotObtainable.New()
} else if StorageUser == "" || StoragePass == "" {
return "", ErrInvalidStorageParams.New()
}
req, err := http.NewRequest("GET", StoragePath, nil)
if err != nil {
return "", ErrStorageRequestFailed.Wrap(err)
}
req.SetBasicAuth(StorageUser, StoragePass)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", ErrStorageRequestFailed.Wrap(err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", ErrStorageRequestFailed.Wrap(err)
}
return strings.TrimSpace(string(data)), nil
}

71
anilist.go Normal file
View File

@ -0,0 +1,71 @@
package logic
import (
"context"
"git.tordarus.net/tordarus/anilist"
"git.tordarus.net/tordarus/channel"
)
func GetAnimeListByAnimeID(statuses []anilist.MediaListStatus) (map[anilist.MediaID]*anilist.MediaList, error) {
animeListChannel, err := GetCurrentlyWatchingAnimes(statuses...)
if err != nil {
return nil, err
}
toMapFunc := func(entry *anilist.MediaList) (anilist.MediaID, *anilist.MediaList) { return entry.MediaID, entry }
return channel.ToMap(animeListChannel, toMapFunc), nil
}
func GetCurrentlyWatchingAnimes(statuses ...anilist.MediaListStatus) (<-chan *anilist.MediaList, error) {
return GetCurrentlyWatchingAnimesContext(context.Background(), statuses...)
}
func GetCurrentlyWatchingAnimesContext(ctx context.Context, statuses ...anilist.MediaListStatus) (<-chan *anilist.MediaList, error) {
token, err := GetAnilistAccessToken()
if err != nil {
return nil, ErrAnimeListNotObtainable.Wrap(err, "access token acquisition failed")
}
media := channel.Map(channel.Of(statuses...), func(status anilist.MediaListStatus) <-chan *anilist.MediaList {
return anilist.NewApi(token).GetMediaList(ctx, anilist.MediaListQuery{
UserName: AnilistUsername,
Type: anilist.MediaTypeAnime,
Status: status,
}, nil).Chan
})
return channel.FlatChan(media), nil
}
var (
animeByTitleCache = map[string]*anilist.Media{}
)
func SearchAnimeByTitle(title string) (anime *anilist.Media, err error) {
// caching
if cacheEntry, ok := animeByTitleCache[title]; ok {
return cacheEntry, nil
}
defer func() {
if err != nil && anime != nil {
animeByTitleCache[title] = anime
}
}()
token, err := GetAnilistAccessToken()
if err != nil {
return nil, err
}
anime = anilist.NewApi(token).GetMedia(context.Background(), anilist.MediaQuery{
Search: title,
Type: anilist.MediaTypeAnime,
}, nil).First()
if anime == nil {
return nil, ErrAnimeNotFound.New(title)
}
return anime, nil
}

31
anime_episode_filepath.go Normal file
View File

@ -0,0 +1,31 @@
package logic
import (
"path/filepath"
"strings"
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/tordarus/anilist"
)
type AnimePathPatternData struct {
Anime *anilist.Media
Episode int
Ext string
}
func GetAnimeEpFilepath(animeEp model.AnimeEpisode, ext string) string {
ext = strings.TrimPrefix(ext, ".")
tmplData := AnimePathPatternData{
Anime: animeEp.Anime,
Episode: animeEp.Episode,
Ext: ext,
}
b := new(strings.Builder)
if err := AnimeEpFilepathPattern.Execute(b, tmplData); err != nil {
panic(err)
}
return filepath.Join(AnimePath, b.String())
}

28
determine_priority.go Normal file
View File

@ -0,0 +1,28 @@
package logic
import "git.tordarus.net/nyaanime/model"
func DeterminePriority(props model.PropertyHolder) (priority int, preferredProperties map[string]int) {
preferredProperties = map[string]int{}
for _, lang := range props.GetLanguages() {
if langPriority, ok := PreferredLanguages[lang]; ok {
priority += langPriority
preferredProperties["lang/"+lang] = langPriority
}
}
for _, sub := range props.GetSubtitles() {
if subPriority, ok := PreferredSubtitles[sub]; ok {
priority += subPriority
preferredProperties["sub/"+sub] = subPriority
}
}
if prefRes, ok := PreferredResolutions[props.GetResolution()]; ok {
priority += prefRes
preferredProperties["res/"+props.GetResolution().String()] = prefRes
}
return
}

66
envvars.go Normal file
View File

@ -0,0 +1,66 @@
package logic
import (
"html/template"
"math"
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/tordarus/anilist"
"git.tordarus.net/tordarus/envvars"
)
var (
AnilistUsername = envvars.String("ANILIST_USERNAME", "username")
AnilistAccessToken = envvars.String("ANILIST_TOKEN", "")
StoragePath = envvars.String("STORAGE_PATH", "")
StorageUser = envvars.String("STORAGE_USERNAME", "")
StoragePass = envvars.String("STORAGE_PASSWORD", "")
AnimePath = envvars.String("ANIME_PATH", "")
AnimeEpFilepathPatternStr = envvars.String("EPISODE_FILEPATH_PATTERN", `{{.Anime.Title.UserPreferred}}/{{.Anime.Title.UserPreferred}} Episode {{.Episode}}.{{.Ext}}`)
AnimeEpFilepathPattern = template.Must(template.New("EPISODE_FILEPATH_PATTERN").Parse(AnimeEpFilepathPatternStr))
// essential torrent properties
MaxResolution = envvars.Object("MAX_RESOLUTION", model.Resolution4K, model.ParseResolution)
MinResolution = envvars.Object("MIN_RESOLUTION", model.ResolutionHD, model.ParseResolution)
EssentialLanguages = envvars.StringSlice("ESSENTIAL_LANGUAGES", "|", []string{})
EssentialSubtitles = envvars.StringSlice("ESSENTIAL_SUBTITLES", "|", []string{})
MaxSeeders = envvars.Int("MAX_SEEDERS", math.MaxInt)
MinSeeders = envvars.Int("MIN_SEEDERS", 0)
MaxLeechers = envvars.Int("MAX_LEECHERS", math.MaxInt)
MinLeechers = envvars.Int("MIN_LEECHERS", 0)
MaxDownloads = envvars.Int("MAX_DOWNLOADS", math.MaxInt)
MinDownloads = envvars.Int("MIN_DOWNLOADS", 0)
TrustedOnly = envvars.Bool("TRUSTED_ONLY", false)
// preferred torrent properties
PreferredLanguages = ParsePreferredStringProps(envvars.StringSlice("PREFERRED_LANGUAGES", "|", []string{}))
PreferredSubtitles = ParsePreferredStringProps(envvars.StringSlice("PREFERRED_SUBTITLES", "|", []string{}))
PreferredResolutions = ParsePreferredProps(envvars.StringSlice("PREFERRED_RESOLUTIONS", "|", []string{}), model.ParseResolution)
AnimeStatuses = envvars.ObjectSlice("ANIME_STATUSES", ",", []anilist.MediaListStatus{
anilist.MediaListStatusCurrent,
anilist.MediaListStatusPlanning,
}, ParseMediaListStatus)
/*
TODO
better idea? implementation in torrent_sort.go (sort.Slice)
PreferredTorrents = envvars.StringSlice("PREFERRED_TORRENTS", []string{"seeders", "subtitles", "languages", "downloads"})
old idea?
PreferMoreLanguages = envvars.Bool("PREFERER_MORE_LANGUAGES", false)
PreferMoreSubtitles = envvars.Bool("PREFERER_MORE_SUBTITLES", false)
PreferMoreSeeders = envvars.Bool("PREFERER_MORE_SEEDERS", false)
PreferMoreDownloads = envvars.Bool("PREFERER_MORE_DOWNLOADS", false)
*/
)

13
errors.go Normal file
View File

@ -0,0 +1,13 @@
package logic
import "git.tordarus.net/tordarus/adverr/v2"
var (
ErrInvalidGlobSyntax = adverr.NewErrTmpl("ErrInvalidGlobSyntax", "invalid filepath.Glob syntax: '%s'")
ErrAnilistTokenNotObtainable = adverr.NewErrTmpl("ErrAnilistTokenNotObtainable", "neither ANILIST_TOKEN nor STORAGE_PATH provided")
ErrAnimeListNotObtainable = adverr.NewErrTmpl("ErrAnimeListNotObtainable", "anime list from anilist.co not obtainable (reason: %s)")
ErrAnimeNotFound = adverr.NewErrTmpl("ErrAnimeNotFound", "could not find anime with name '%s'")
ErrInvalidStorageParams = adverr.NewErrTmpl("ErrInvalidStorageParams", "STORAGE_USER or STORAGE_PASS not provided")
ErrStorageRequestFailed = adverr.NewErrTmpl("ErrStorageRequestFailed", "request to file storage could not be made")
ErrInvalidAnimeStatus = adverr.NewErrTmpl("ErrInvalidAnimeStatus", "invalid status '%s' in ANIME_STATUS (allowed: %s)")
)

14
esc_seq.go Normal file
View File

@ -0,0 +1,14 @@
package logic
import "strings"
var EscSeqReplacer = strings.NewReplacer(
`\\`, `\`,
`\n`, "\n",
`\t`, "\t",
`\f`, "\f",
`\r`, "\r",
`\v`, "\v",
`\b`, "\b",
`\a`, "\a",
)

21
file_priority.go Normal file
View File

@ -0,0 +1,21 @@
package logic
import (
"git.tordarus.net/nyaanime/model"
)
type FilePriority struct {
Properties model.PropertyHolder
Priority int
PreferredProperties map[string]int
}
func NewFilePriority(props model.PropertyHolder) *FilePriority {
priority, preferredProperties := DeterminePriority(props)
return &FilePriority{
Properties: props,
Priority: priority,
PreferredProperties: preferredProperties,
}
}

58
file_props.go Normal file
View File

@ -0,0 +1,58 @@
package logic
import (
"path/filepath"
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/nyaanime/parsers"
"git.tordarus.net/tordarus/slices"
)
func GetAnimeEpProps(animeEp model.AnimeEpisode) (*FilePriority, bool) {
animeEpPath := GetAnimeEpFilepath(animeEp, "*")
files, err := filepath.Glob(animeEpPath)
if err != nil {
panic(ErrInvalidGlobSyntax.Wrap(err, animeEpPath))
}
var mostPrio *FilePriority
for _, file := range files {
props, err := parsers.AnalyzeFile(file)
if err != nil {
continue
}
// if !HasFileEssentialProperties(props) {
// continue
// }
fp := NewFilePriority(props)
if mostPrio == nil || fp.Priority > mostPrio.Priority {
mostPrio = fp
}
}
return mostPrio, mostPrio != nil
}
func HasFileEssentialProperties(props model.PropertyHolder) bool {
if props.GetResolution() < MinResolution || props.GetResolution() > MaxResolution {
return false
}
for _, essentialLanguage := range EssentialLanguages {
if !slices.Contains(props.GetLanguages(), essentialLanguage) {
return false
}
}
for _, essentialSubtitle := range EssentialSubtitles {
if !slices.Contains(props.GetSubtitles(), essentialSubtitle) {
return false
}
}
return true
}

23
go.mod Normal file
View File

@ -0,0 +1,23 @@
module git.tordarus.net/nyaanime/logic
go 1.23
toolchain go1.24.3
require (
git.tordarus.net/nyaanime/model v0.0.1
git.tordarus.net/nyaanime/parsers v0.0.2
git.tordarus.net/tordarus/adverr/v2 v2.0.2
git.tordarus.net/tordarus/anilist v1.5.2
git.tordarus.net/tordarus/channel v0.1.19
git.tordarus.net/tordarus/envvars v0.0.0-20250114175450-d73e12b838a5
git.tordarus.net/tordarus/gmath v0.0.7
git.tordarus.net/tordarus/slices v0.0.14
git.tordarus.net/tordarus/tprint v0.0.1
)
require (
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
gopkg.in/vansante/go-ffprobe.v2 v2.2.1 // indirect
)

24
go.sum Normal file
View File

@ -0,0 +1,24 @@
git.tordarus.net/nyaanime/model v0.0.1 h1:/I+87Z6eEw/o2adltKnCk4FZai2mPekjYlzEjY1ppyQ=
git.tordarus.net/nyaanime/model v0.0.1/go.mod h1:oHV82UMNy4XgPHkI6tZiwabdi6myqHXgjMi9sNZ+rG4=
git.tordarus.net/nyaanime/parsers v0.0.2 h1:UxfDGxgS2guldLhRtlLNjst/UyeA8OC44oj7nUeRUB8=
git.tordarus.net/nyaanime/parsers v0.0.2/go.mod h1:sx8HyJCpG7zzwRAEE1ZlmVPirPc3fdArlCM5L1sxEaQ=
git.tordarus.net/tordarus/adverr/v2 v2.0.2 h1:7nvNjMMjtGPq0EY6duMiv+seJ7MacNvKSBmckHl6Erg=
git.tordarus.net/tordarus/adverr/v2 v2.0.2/go.mod h1:gCC46KsWosZJh7MVNDEU99hKQoxEWZgHITDHtmFwwiQ=
git.tordarus.net/tordarus/anilist v1.5.2 h1:SxlovS+e3lgL2SowQQwj8dQrIZzRFPomcGCw3V+My0Q=
git.tordarus.net/tordarus/anilist v1.5.2/go.mod h1:Mrhx/9+8HJVj5ebQ5fJuXqL220tEJhgQIqFK2WKPXgA=
git.tordarus.net/tordarus/channel v0.1.19 h1:d9xnSwFyvBh4B1/82mt0A7Gpm2nIZJTc+9ceJMIOu5Q=
git.tordarus.net/tordarus/channel v0.1.19/go.mod h1:8/dWFTdGO7g4AeSZ7cF6GerkGbe9c4dBVMVDBxOd9m4=
git.tordarus.net/tordarus/envvars v0.0.0-20250114175450-d73e12b838a5 h1:rKNDX/YGunqg8TEU6q1rgS2BcDKVmUW2cg61JOE/wws=
git.tordarus.net/tordarus/envvars v0.0.0-20250114175450-d73e12b838a5/go.mod h1:/qVGwrEmqtIrZyuuoIQl4vquSkPWUNJmlGNedDrdYfg=
git.tordarus.net/tordarus/gmath v0.0.7 h1:tR48idt9AUL0r556ww3ZxByTKJEr6NWCTlhl2ihzYxQ=
git.tordarus.net/tordarus/gmath v0.0.7/go.mod h1:mO7aPlvNrGVE9UFXEuuACjZgMDsM63l3OcQy6xSQnoE=
git.tordarus.net/tordarus/slices v0.0.14 h1:Jy1VRMs777WewJ7mxTgjyQIMm/Zr+co18/XoQ01YZ3A=
git.tordarus.net/tordarus/slices v0.0.14/go.mod h1:RgE7A1aSAezIvPUgcbUuMHu0q4xGKoRevT+DC0eJmwI=
git.tordarus.net/tordarus/tprint v0.0.1 h1:aM5c0nLwicUIoic/xguwE5fQdQ2bB3z0+FQEN/Yt0H4=
git.tordarus.net/tordarus/tprint v0.0.1/go.mod h1:2UdHVY/ue8vXeJU/IJY1xBikDaH35kaMzxjk9ryKB8Q=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
gopkg.in/vansante/go-ffprobe.v2 v2.2.1 h1:sFV08OT1eZ1yroLCZVClIVd9YySgCh9eGjBWO0oRayI=
gopkg.in/vansante/go-ffprobe.v2 v2.2.1/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE=

53
preferred_props.go Normal file
View File

@ -0,0 +1,53 @@
package logic
import (
"strings"
"git.tordarus.net/tordarus/gmath"
"git.tordarus.net/tordarus/slices"
)
// ParsePreferredProps parses properties and its corresponding priority.
// priorities are distributed exponentially in reverse order.
//
// That means the last entry will have priority 1, the second last 2, then 4, 8 and so on.
//
// Properties with name "_" will be ignored and function as a placeholder to increase the priority
// of the properties which comes before them.
//
// Properties separated by comma will have the same priorities.
//
// str usually is the return value of a call to strings.Split(str, "|")
//
// Examples:
// str = "a|b|c" -> c:1 b:2 a:4
// str = "a|b|_|c" -> c:1 b:4 a:8
// str = "a,b|c" -> c:1 b:4 a:4
// str = "d|_|a,b|c" -> c:1 b:4 a:4 d:16
//
// Additionally, properties can be converted to a generic type with the converter function
func ParsePreferredProps[T comparable](str []string, converter func(string) (T, error)) map[T]int {
props := map[T]int{}
for i, subProps := range slices.Reverse(str) {
if subProps == "_" {
continue
}
propPriority := gmath.Pow(2, i)
for _, subProp := range strings.Split(subProps, ",") {
subPropT, err := converter(subProp)
if err != nil {
continue
}
props[subPropT] = propPriority
}
}
return props
}
func ParsePreferredStringProps(str []string) map[string]int {
return ParsePreferredProps(str, func(s string) (string, error) { return s, nil })
}

59
utils.go Normal file
View File

@ -0,0 +1,59 @@
package logic
import (
"fmt"
"sort"
"strings"
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/tordarus/anilist"
"git.tordarus.net/tordarus/slices"
"git.tordarus.net/tordarus/tprint"
)
var AllMediaListStatuses = []anilist.MediaListStatus{
anilist.MediaListStatusCurrent,
anilist.MediaListStatusPlanning,
anilist.MediaListStatusCompleted,
anilist.MediaListStatusDropped,
anilist.MediaListStatusPaused,
anilist.MediaListStatusRepeating,
}
func ParseMediaListStatus(str string) (anilist.MediaListStatus, error) {
s := anilist.MediaListStatus(strings.ToUpper(str))
allStatusesStr := slices.Map(AllMediaListStatuses, func(status anilist.MediaListStatus) string {
return string(status)
})
if !slices.Contains(AllMediaListStatuses, s) {
return s, ErrInvalidAnimeStatus.New(s, strings.Join(allStatusesStr, ","))
}
return s, nil
}
func Map2Table[K comparable](title string, m map[K]int) string {
table := tprint.NewTable(title, "priority")
entries := make([]model.Pair[K, int], 0, len(m))
for name, priority := range m {
entries = append(entries, model.Pair[K, int]{First: name, Second: priority})
}
sort.Slice(entries, func(i, j int) bool { return entries[i].Second > entries[j].Second })
for _, entry := range entries {
table.AddRow(entry.First, entry.Second)
}
return table.String()
}
func PrintPriorityTables() {
fmt.Println("generated priority values:")
fmt.Print(Map2Table("language", PreferredLanguages))
fmt.Print(Map2Table("subtitle", PreferredSubtitles))
fmt.Print(Map2Table("resolution", PreferredResolutions))
fmt.Println()
}