From 9934d85022ab83f028ef1547bc60cbdd5519482e Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Mon, 15 Aug 2022 15:55:27 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + access_token.go | 39 ++++++++++++++++++++ anilist.go | 69 ++++++++++++++++++++++++++++++++++ errors.go | 14 +++++++ go.mod | 20 ++++++++++ go.sum | 63 +++++++++++++++++++++++++++++++ main.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++ nyaa.go | 79 +++++++++++++++++++++++++++++++++++++++ torrent_filter.go | 54 +++++++++++++++++++++++++++ torrent_parse.go | 53 ++++++++++++++++++++++++++ torrent_sort.go | 9 +++++ utils.go | 6 +++ 12 files changed, 501 insertions(+) create mode 100644 .gitignore create mode 100644 access_token.go create mode 100644 anilist.go create mode 100644 errors.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 nyaa.go create mode 100644 torrent_filter.go create mode 100644 torrent_parse.go create mode 100644 torrent_sort.go create mode 100644 utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/access_token.go b/access_token.go new file mode 100644 index 0000000..c41bb2c --- /dev/null +++ b/access_token.go @@ -0,0 +1,39 @@ +package main + +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 +} diff --git a/anilist.go b/anilist.go new file mode 100644 index 0000000..fc645e0 --- /dev/null +++ b/anilist.go @@ -0,0 +1,69 @@ +package main + +import ( + "context" + + "git.milar.in/milarin/anilist" + "git.milar.in/milarin/channel" +) + +func GetCurrentlyWatchingAnimesByAnimeID() (map[anilist.MediaID]*anilist.MediaList, error) { + watchingAnimesChannel, err := GetCurrentlyWatchingAnimes() + if err != nil { + return nil, err + } + + toMapFunc := func(entry *anilist.MediaList) (anilist.MediaID, *anilist.MediaList) { return entry.MediaID, entry } + return channel.ToMap(watchingAnimesChannel, toMapFunc), nil +} + +func GetCurrentlyWatchingAnimes() (<-chan *anilist.MediaList, error) { + token, err := GetAnilistAccessToken() + if err != nil { + return nil, ErrAnimeListNotObtainable.Wrap(err, "access token acquisition failed") + } + + currentMedia := anilist.NewApi(token).GetMediaList(context.Background(), anilist.MediaListQuery{ + UserName: AnilistUsername, + Type: anilist.MediaTypeAnime, + Status: anilist.MediaListStatusCurrent, + }, nil) + + plannedMedia := anilist.NewApi(token).GetMediaList(context.Background(), anilist.MediaListQuery{ + UserName: AnilistUsername, + Type: anilist.MediaTypeAnime, + Status: anilist.MediaListStatusPlanning, + }, nil) + + // TODO add completedMedia for re-downloading last episode? + + return channel.Merge(currentMedia.Chan, plannedMedia.Chan), nil +} + +var ( + animeByTitleCache = map[string]Pair[*anilist.Media, error]{} +) + +func SearchAnimeByTitle(title string) (anime *anilist.Media, err error) { + // caching + if cacheEntry, ok := animeByTitleCache[title]; ok { + return cacheEntry.First, cacheEntry.Second + } + defer func() { animeByTitleCache[title] = Pair[*anilist.Media, error]{anime, err} }() + + 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 +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..f020df3 --- /dev/null +++ b/errors.go @@ -0,0 +1,14 @@ +package main + +import "git.tordarus.net/Tordarus/adverr" + +var ( + ErrNoSuitableParser = adverr.NewErrTmpl("ErrNoSuitableParser", "could not parse torrent with ID %s because no suitable parser found") + ErrTorrentParseFailed = adverr.NewErrTmpl("ErrTorrentParseFailed", "could not parse torrent with ID %s (parsed with '%s')") + ErrAnimeNotFound = adverr.NewErrTmpl("ErrAnimeNotFound", "could not find anime with name '%s'") + ErrAnilistTokenNotObtainable = adverr.NewErrTmpl("ErrAnilistTokenNotObtainable", "neither ANILIST_TOKEN nor STORAGE_PATH provided") + ErrInvalidStorageParams = adverr.NewErrTmpl("ErrInvalidStorageParams", "STORAGE_USER or STORAGE_PASS not provided") + ErrStorageRequestFailed = adverr.NewErrTmpl("ErrStorageRequestFailed", "request to file storage could not be made") + ErrAnimeListNotObtainable = adverr.NewErrTmpl("ErrAnimeListNotObtainable", "anime list from anilist.co not obtainable (reason: %s)") + ErrTorrentNotObtainable = adverr.NewErrTmpl("ErrTorrentNotObtainable", "torrents from nyaa.si not obtainable (reason: %s)") +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..578ea0b --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module git.milar.in/animan/downloader + +go 1.18 + +require ( + git.milar.in/animan/model v0.0.0-20220815125824-0716848c4a4d + git.milar.in/animan/parsers v0.0.0-20220815122943-517518dd3064 + git.milar.in/milarin/adverr v0.2.1 + git.milar.in/milarin/anilist v1.5.0 + git.milar.in/milarin/channel v0.0.7 + git.milar.in/milarin/envvars v1.0.3 + git.tordarus.net/Tordarus/adverr v0.2.0 + github.com/PuerkitoBio/goquery v1.8.0 + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e +) + +require ( + github.com/andybalholm/cascadia v1.3.1 // indirect + golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7433d90 --- /dev/null +++ b/go.sum @@ -0,0 +1,63 @@ +git.milar.in/animan/model v0.0.0-20220803215306-905563410463 h1:M7Xxqbv7WQ3xwW66APwI7KQ8wD8IN/v6ZZbq1JB1T2M= +git.milar.in/animan/model v0.0.0-20220803215306-905563410463/go.mod h1:FmgisUVGJd8EEvGP4uk9pEv6Ic9aiEhAy5eZF8zZsgU= +git.milar.in/animan/model v0.0.0-20220803222302-20b8d45495c4 h1:dh98Tu+uSh02ivBsGKPq6klzPTWP4oHieUPYpMHxxtQ= +git.milar.in/animan/model v0.0.0-20220803222302-20b8d45495c4/go.mod h1:FmgisUVGJd8EEvGP4uk9pEv6Ic9aiEhAy5eZF8zZsgU= +git.milar.in/animan/model v0.0.0-20220804134914-691506ccc5f0 h1:Q3Co0PrCAmPGtBSW/p8j4xns8i7bs8yIxnwqiajfN7Y= +git.milar.in/animan/model v0.0.0-20220804134914-691506ccc5f0/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/model v0.0.0-20220804144536-96f16f9e0584 h1:T7xYzMtu15ZzFM9ERhuN+5V2ZMFBGalb+2/13ShXtOY= +git.milar.in/animan/model v0.0.0-20220804144536-96f16f9e0584/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/model v0.0.0-20220804154959-f8b66e8c9eda h1:9XG7Dn+UtyLwwNcuol10EdHcBQjYD+1WzmnJbRUJQfs= +git.milar.in/animan/model v0.0.0-20220804154959-f8b66e8c9eda/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/model v0.0.0-20220804163811-82537cb1f6e4 h1:fdtvkK2LJ9O3bP20wcqsL8gpOFr5nLoCn+Qb+SbyilU= +git.milar.in/animan/model v0.0.0-20220804163811-82537cb1f6e4/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/model v0.0.0-20220815093549-fe9b565a9dd2 h1:vNbYNp8VJQ6FBENgZMHA1IFOfPkQEA+TIaN8PFF8m48= +git.milar.in/animan/model v0.0.0-20220815093549-fe9b565a9dd2/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/model v0.0.0-20220815103315-0378bec21c68 h1:UiX2wucO/SkWHCuaxFGr7hw5yqhjR8LYk5gxesRDFec= +git.milar.in/animan/model v0.0.0-20220815103315-0378bec21c68/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/model v0.0.0-20220815122339-9ce113fc9042 h1:rYaoxRO2rKuUm199yq/sIhRRZqe/9kXDSRbFHdWKeMM= +git.milar.in/animan/model v0.0.0-20220815122339-9ce113fc9042/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/model v0.0.0-20220815123155-23f657024bd0 h1:lWAANKFbwvZah9HHOE851FXNHbId8tbv7OGwmH+BBrE= +git.milar.in/animan/model v0.0.0-20220815123155-23f657024bd0/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/model v0.0.0-20220815124104-e3f2c831ee81 h1:b6xJxC2GCB0NgVV0Smh5lmhesB1qSen2bje/570P0Wk= +git.milar.in/animan/model v0.0.0-20220815124104-e3f2c831ee81/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/model v0.0.0-20220815125235-67e9015e4253 h1:ZCz7aBf6v6pCVavPsUULsvqi84DAo9PpjafmUjfIH6w= +git.milar.in/animan/model v0.0.0-20220815125235-67e9015e4253/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/model v0.0.0-20220815125506-350681318c43 h1:z6BId1oJWLRj4PalwtsVPG8Ezm2YYot82YFzrd3Iymk= +git.milar.in/animan/model v0.0.0-20220815125506-350681318c43/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/model v0.0.0-20220815125824-0716848c4a4d h1:oy50tLPhaLG0zKrrlQcsPbjfbgtXi7g9LK5NJt+v5qE= +git.milar.in/animan/model v0.0.0-20220815125824-0716848c4a4d/go.mod h1:oXDr3slnzXoccIrci2wotX0cWwMVuAQ9dSvmh1buE4c= +git.milar.in/animan/parsers v0.0.0-20220803220519-b516177d7cff h1:KN5IznBS1xvqrwjZNmp71FyfqkILvHgwkOreoqQ5zFk= +git.milar.in/animan/parsers v0.0.0-20220803220519-b516177d7cff/go.mod h1:2x+xW6lKKkVVG39sbJFR2B7r7WxHkbmwxyVIkj1R1h4= +git.milar.in/animan/parsers v0.0.0-20220804161341-2a3194c37eb6 h1:TNXl6LGxACimnllnoBgPEV7PpMv//+Evqte7C8K4X1M= +git.milar.in/animan/parsers v0.0.0-20220804161341-2a3194c37eb6/go.mod h1:X2VK8tKVbtA8nhzjJfRjXZycE4HMwDHnAUWQXU7/gIU= +git.milar.in/animan/parsers v0.0.0-20220815114013-d4e5e659350a h1:73ELbFmLE3ejVkiTimDzIz2n6FI1K6raMc4KRw1wJOc= +git.milar.in/animan/parsers v0.0.0-20220815114013-d4e5e659350a/go.mod h1:ffYwJnlRMtebp4wE01i+Tyz85yVasw0RSUkG38XeKdI= +git.milar.in/animan/parsers v0.0.0-20220815122943-517518dd3064 h1:1gbx3Tzrw8zb5lspRG/vzJ1KdosSOR3brP+9IoXII9g= +git.milar.in/animan/parsers v0.0.0-20220815122943-517518dd3064/go.mod h1:ffYwJnlRMtebp4wE01i+Tyz85yVasw0RSUkG38XeKdI= +git.milar.in/milarin/adverr v0.2.1 h1:eyXFGC+Ui/kcNt2+NqP3HiAplwxzqeNr9DfitsUb3c4= +git.milar.in/milarin/adverr v0.2.1/go.mod h1:wwfglcey4R3vqjNL/d8mbnvFJGzETRXzAEolIHZY32w= +git.milar.in/milarin/anilist v1.5.0 h1:fSiAXY/topNk4ISEp2QtcG9HHKLJfMc8w05iqc+Paf0= +git.milar.in/milarin/anilist v1.5.0/go.mod h1:8PTHXFMA45JpfRFIpcdrKwDHue8fbT/wwV1BuHFn6c0= +git.milar.in/milarin/channel v0.0.7 h1:cVKtwgH/EE7U+XTHcoFCClJ4LR349KanzjX9xKwRcNg= +git.milar.in/milarin/channel v0.0.7/go.mod h1:We83LTI8S7u7II3pD+A2ChCDWJfCkcBUCUqii9HjTtM= +git.milar.in/milarin/envvars v1.0.2 h1:FjN9qczW7u2LVzJv/hYf8a4m1MZMUkFiqvEjzHJDYgc= +git.milar.in/milarin/envvars v1.0.2/go.mod h1:rLh/HN6S254h6m2lklnImcpsy4kHFxaOjM6+Nv9GHKI= +git.milar.in/milarin/envvars v1.0.3 h1:go6pYExUzPx+aLvJ5BKNkaANkBNmcrlRJ8TtWsk4uWY= +git.milar.in/milarin/envvars v1.0.3/go.mod h1:rLh/HN6S254h6m2lklnImcpsy4kHFxaOjM6+Nv9GHKI= +git.tordarus.net/Tordarus/adverr v0.2.0 h1:kLYjR2/Vb2GHiSAMvAv+WPNaHR9BRphKanf8H/pCZdA= +git.tordarus.net/Tordarus/adverr v0.2.0/go.mod h1:XRf0+7nhOkIEr0gi9DUG4RvV2KaOFB0fYPDaR1KLenw= +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E= +golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..8c8b703 --- /dev/null +++ b/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + "math" + "time" + + "git.milar.in/animan/model" + "git.milar.in/milarin/adverr" + "git.milar.in/milarin/envvars" +) + +var ( + PollRate = envvars.Object("POLL_RATE", 30*time.Minute, time.ParseDuration) + + AnilistUsername = envvars.String("ANILIST_USERNAME", "username") + AnilistAccessToken = envvars.String("ANILIST_TOKEN", "") + + StoragePath = envvars.String("STORAGE_PATH", "") + StorageUser = envvars.String("STORAGE_USER", "") + StoragePass = envvars.String("STORAGE_PASS", "") + + // 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", ",") + EssentialSubtitles = envvars.StringSlice("ESSENTIAL_SUBTITLES", ",") + + 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 = envvars.StringSlice("PREFERRED_LANGUAGES", ",") + PreferredSubtitles = envvars.StringSlice("PREFERRED_SUBTITLES", ",") + + /* + TODO + + PreferMoreLanguages = envvars.Bool("PREFERER_MORE_LANGUAGES", false) + PreferMoreSubtitles = envvars.Bool("PREFERER_MORE_SUBTITLES", false) + */ +) + +func main() { + // get access token once at startup to be sure that an access token is obtainable at all + if _, err := GetAnilistAccessToken(); err != nil { + panic(err) + } + + ticker := time.NewTicker(PollRate) + defer ticker.Stop() + + checkTorrents() + for range ticker.C { + checkTorrents() + } +} + +func checkTorrents() { + fmt.Println("check torrents") + + torrents, err := GetLatestNyaaContent() + if err != nil { + fmt.Println(adverr.Wrap("retrieving torrents failed", err)) + return + } + + /*watchingAnimes, err := GetCurrentlyWatchingAnimesByAnimeID() + if err != nil { + fmt.Println(adverr.Wrap("retrieving anime list failed", err)) + return + }*/ + + parsedTorrentsByAnimeEp := FilterEssentialTorrents(ParseTorrentsByAnimeEp(torrents)) + + for _, parsedTorrents := range parsedTorrentsByAnimeEp { + for _, parsedTorrent := range parsedTorrents { + fmt.Println(parsedTorrent, "|", parsedTorrent.Torrent.StringWithoutTitle()) + } + } + + fmt.Printf("sleep for %s\n", PollRate) +} diff --git a/nyaa.go b/nyaa.go new file mode 100644 index 0000000..b2496c9 --- /dev/null +++ b/nyaa.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "git.milar.in/animan/model" + "github.com/PuerkitoBio/goquery" +) + +var torrentLinkRegex = regexp.MustCompile(`https:\/\/nyaa\.si\/download\/(\d+?)\.torrent`) + +func GetLatestNyaaContent() ([]model.Torrent, error) { + resp, err := http.Get("https://nyaa.si/?page=rss&f=0&c=0_0&q=Erai-Raws+ayumu") // TODO https://nyaa.si/?page=rss + if err != nil { + return nil, ErrTorrentNotObtainable.Wrap(err, "torrent data acqusition failed") + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return nil, ErrTorrentNotObtainable.New("invalid status code from nyaa.si: " + strconv.Itoa(resp.StatusCode)) + } + + nyaa, err := goquery.NewDocumentFromReader(resp.Body) + if err != nil { + return nil, ErrTorrentNotObtainable.Wrap(err, "nyaa.si response parsing failed") + } + + torrents := make([]model.Torrent, 0, 75) + + nyaa.Find("item").Each(func(i int, s *goquery.Selection) { + time, err := time.Parse(time.RFC1123Z, s.Find("pubDate").Text()) + if err != nil { + fmt.Println("could not parse time:", s.Find("pubDate")) + return + } + + seeders, err := strconv.Atoi(s.Find("nyaa\\:seeders").Text()) + if err != nil { + fmt.Println("could not parse seeders:", s.Find("nyaa\\:seeders").Text()) + return + } + + leechers, err := strconv.Atoi(s.Find("nyaa\\:leechers").Text()) + if err != nil { + fmt.Println("could not parse leechers:", s.Find("nyaa\\:leechers").Text()) + return + } + + downloads, err := strconv.Atoi(s.Find("nyaa\\:downloads").Text()) + if err != nil { + fmt.Println("could not parse downloads:", s.Find("nyaa\\:downloads").Text()) + return + } + + // goquery can't parse the link tag for some reason + // therefore I have to get around this bug by exploiting regex + + matches := torrentLinkRegex.FindStringSubmatch(s.Text()) + link := matches[0] + id := matches[1] + + torrents = append(torrents, model.Torrent{ + ID: model.TorrentID(id), + Title: s.Find("title").Text(), + Link: link, + Time: time, + Seeders: seeders, + Leechers: leechers, + Downloads: downloads, + Trusted: strings.Contains(strings.ToLower(s.Find("nyaa\\:trusted").Text()), "yes"), + }) + }) + + return torrents, nil +} diff --git a/torrent_filter.go b/torrent_filter.go new file mode 100644 index 0000000..bc17f9e --- /dev/null +++ b/torrent_filter.go @@ -0,0 +1,54 @@ +package main + +import ( + "git.milar.in/animan/model" + "golang.org/x/exp/slices" +) + +func FilterEssentialTorrents(parsedTorrentsByAnimeEp map[model.AnimeEpisode][]*model.ParsedTorrent) map[model.AnimeEpisode][]*model.ParsedTorrent { + filteredMap := map[model.AnimeEpisode][]*model.ParsedTorrent{} + for animeEpisode, parsedTorrents := range parsedTorrentsByAnimeEp { + for _, parsedTorrent := range parsedTorrents { + if HasEssentialProperties(parsedTorrent) { + filteredMap[animeEpisode] = append(filteredMap[animeEpisode], parsedTorrent) + } + } + } + return filteredMap +} + +func HasEssentialProperties(torrent *model.ParsedTorrent) bool { + if torrent.Resolution < MinResolution || torrent.Resolution > MaxResolution { + return false + } + + if torrent.Torrent.Seeders < MinSeeders || torrent.Torrent.Seeders > MaxSeeders { + return false + } + + if torrent.Torrent.Leechers < MinLeechers || torrent.Torrent.Leechers > MaxLeechers { + return false + } + + if torrent.Torrent.Downloads < MinDownloads || torrent.Torrent.Downloads > MaxDownloads { + return false + } + + if TrustedOnly && torrent.Torrent.Trusted { + return false + } + + for _, essentialLanguage := range EssentialLanguages { + if !slices.Contains(torrent.Languages, essentialLanguage) { + return false + } + } + + for _, essentialSubtitle := range EssentialSubtitles { + if !slices.Contains(torrent.Subtitles, essentialSubtitle) { + return false + } + } + + return true +} diff --git a/torrent_parse.go b/torrent_parse.go new file mode 100644 index 0000000..73e017c --- /dev/null +++ b/torrent_parse.go @@ -0,0 +1,53 @@ +package main + +import ( + "errors" + "fmt" + + "git.milar.in/animan/model" + "git.milar.in/animan/parsers" + "git.milar.in/milarin/adverr" +) + +func ParseTorrent(torrent *model.Torrent) (*model.ParsedTorrent, error) { + for _, parser := range parsers.Parsers { + parsedTorrent, ok := parser.TorrentParser(&parser, torrent) + if ok { + anime, err := SearchAnimeByTitle(parsedTorrent.OriginalAnimeTitle) + if err != nil { + return parsedTorrent, ErrTorrentParseFailed.Wrap(err, torrent.ID, parser.String()) + } + + parsedTorrent.Anime = anime + return parsedTorrent, nil + } + } + + return nil, ErrNoSuitableParser.New(torrent.ID) +} + +func ParseTorrentsByAnimeEp(torrents []model.Torrent) map[model.AnimeEpisode][]*model.ParsedTorrent { + torrentsByAnimeEp := map[model.AnimeEpisode][]*model.ParsedTorrent{} + + for _, torrent := range torrents { + torrent := torrent + parsedTorrent, err := ParseTorrent(&torrent) + if err != nil { + if errors.Is(err, ErrAnimeNotFound) { + fmt.Printf("anime not found: \"%s\" (torrent ID: %s)\n", parsedTorrent.OriginalAnimeTitle, torrent.ID) + } else if !errors.Is(err, ErrNoSuitableParser) { + adverr.Println(err) + } + continue + } + + animeEpisode := model.AnimeEpisode{ + Anime: parsedTorrent.Anime.Title.Native, + Episode: parsedTorrent.Episode, + } + + torrentsByAnimeEp[animeEpisode] = append(torrentsByAnimeEp[animeEpisode], parsedTorrent) + } + + return torrentsByAnimeEp +} diff --git a/torrent_sort.go b/torrent_sort.go new file mode 100644 index 0000000..fb79c3b --- /dev/null +++ b/torrent_sort.go @@ -0,0 +1,9 @@ +package main + +import "git.milar.in/animan/model" + +func SortTorrentsByPreferredProperties(torrents map[model.AnimeEpisode][]*model.ParsedTorrent) { + // TODO + // sort torrent slices by preferred properties + // store prefered properties in db somewhere +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..82392ab --- /dev/null +++ b/utils.go @@ -0,0 +1,6 @@ +package main + +type Pair[A, B any] struct { + First A + Second B +}