initial commit

This commit is contained in:
2025-06-06 16:45:59 +02:00
commit 8dccbf25b8
18 changed files with 835 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.env

23
anime_episode_cache.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/tordarus/anilist"
)
var AnimeEpisodeCache = map[anilist.MediaID]map[int]model.AnimeEpisode{}
func GetAnimeEpisode(anime *anilist.Media, episode int) model.AnimeEpisode {
if _, ok := AnimeEpisodeCache[anime.ID]; !ok {
AnimeEpisodeCache[anime.ID] = map[int]model.AnimeEpisode{}
}
if _, ok := AnimeEpisodeCache[anime.ID][episode]; !ok {
AnimeEpisodeCache[anime.ID][episode] = model.AnimeEpisode{
Anime: anime,
Episode: episode,
}
}
return AnimeEpisodeCache[anime.ID][episode]
}

93
check_torrents.go Normal file
View File

@ -0,0 +1,93 @@
package main
import (
"fmt"
"log"
"sort"
"time"
"git.tordarus.net/nyaanime/logic"
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/tordarus/adverr/v2"
"git.tordarus.net/tordarus/slices"
)
func CheckTorrents() {
log.Println("checking torrents")
start := time.Now()
torrents, err := GetTorrents()
if err != nil {
fmt.Println(adverr.Wrap("retrieving torrents failed", err))
return
}
animeList, err := logic.GetAnimeListByAnimeID(logic.AnimeStatuses)
if err != nil {
fmt.Println(adverr.Wrap("retrieving anime list failed", err))
return
}
// parse torrents
parsedTorrentsByAnimeEp := ParseTorrentsByAnimeEp(torrents)
// filter not on anime list
animeListTorrents := FilterTorrentsByAnimeList(parsedTorrentsByAnimeEp, animeList)
// filter essential properties
essentialTorrents := FilterEssentialTorrents(animeListTorrents)
// filter preferred properties
preferredTorrents := GetTorrentsWithMaxPrioByAnimeEp(essentialTorrents)
// information gathered for logging purposes
downloadingEpisodes := map[model.AnimeEpisode]struct{}{}
inCollectionEpisodes := map[model.AnimeEpisode]bool{}
collectionPreferredTorrents := map[*model.ParsedTorrent]bool{}
downloadedTorrents := map[model.AnimeEpisode]*TorrentPriority{}
for animeEp, torrentPrio := range preferredTorrents {
if IsCurrentlyDownloading(animeEp) {
downloadingEpisodes[animeEp] = struct{}{} // debug output
continue
}
props, inCollection := logic.GetAnimeEpProps(animeEp)
inCollectionEpisodes[animeEp] = inCollection // debug output
if inCollection {
collectionPreferred := props.Priority >= torrentPrio.Priority
collectionPreferredTorrents[torrentPrio.ParsedTorrent] = collectionPreferred // debug output
if collectionPreferred {
continue
}
}
if err := DownloadTorrent(animeEp, torrentPrio.ParsedTorrent); err != nil {
fmt.Println(fmt.Sprintf("could not download torrent %s", torrentPrio.ParsedTorrent.Torrent.ID), err)
}
downloadedTorrents[animeEp] = torrentPrio // debug output
}
duration := time.Since(start)
ShowDebugInfo(
parsedTorrentsByAnimeEp,
animeListTorrents,
essentialTorrents,
preferredTorrents,
downloadingEpisodes,
inCollectionEpisodes,
collectionPreferredTorrents,
downloadedTorrents,
)
downloadedAnimeEpisodes := slices.OfMap(downloadedTorrents, func(animeEp model.AnimeEpisode, torrentPrio *TorrentPriority) model.AnimeEpisode {
return animeEp
})
sort.Slice(downloadedAnimeEpisodes, GetAnimeEpisodesSortFunc(downloadedAnimeEpisodes))
SendTelegramAnimeEpMessage(TelegramDownloadMessagePattern, downloadedAnimeEpisodes)
log.Printf("check took %s. sleeping for %s\n", duration.Truncate(time.Millisecond), (PollRate - duration).Truncate(time.Millisecond))
}

40
download_torrent_file.go Normal file
View File

@ -0,0 +1,40 @@
package main
import (
"io"
"net/http"
"os"
"path"
"path/filepath"
"git.tordarus.net/nyaanime/model"
)
func DownloadTorrent(animeEp model.AnimeEpisode, torrent *model.ParsedTorrent) error {
if err := SetCurrentlyDownloading(animeEp); err != nil {
return ErrLockFileCreationFailed.Wrap(err, animeEp.Anime.Title.Romaji, animeEp.Episode)
}
torrentFilePath := filepath.Join(TorrentPath, path.Base(torrent.Torrent.Link))
//fmt.Printf("download: %s -> %s\n", torrent.Torrent.Link, torrentFilePath)
resp, err := http.Get(torrent.Torrent.Link)
if err != nil {
return ErrDownloadTorrentFileFailed.Wrap(err, torrent.Torrent.Link)
}
defer resp.Body.Close()
file, err := os.Create(torrentFilePath)
if err != nil {
return ErrSaveTorrentFileFailed.Wrap(err, torrent.Torrent.Link)
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
return ErrSaveTorrentFileFailed.Wrap(err, torrent.Torrent.Link)
}
return nil
}

25
envvars.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"text/template"
"time"
"git.tordarus.net/nyaanime/logic"
"git.tordarus.net/tordarus/envvars"
)
var (
PollRate = envvars.Object("POLL_RATE", 30*time.Minute, time.ParseDuration)
TorrentPath = envvars.String("TORRENT_PATH", "")
DebugAnimeEpisodePatternStr = envvars.String("DEBUG_ANIME_EPISODE_PATTERN", `{{.Anime.Title.UserPreferred}} episode {{.Episode}}`)
DebugAnimeEpisodePattern = template.Must(template.New("DEBUG_ANIME_EPISODE_PATTERN").Parse(DebugAnimeEpisodePatternStr))
TelegramBotToken = envvars.String("TELEGRAM_API_TOKEN", "")
TelegramChatID = envvars.Int64("TELEGRAM_CHAT_ID", 0)
TelegramDownloadMessagePatternStr = logic.EscSeqReplacer.Replace(envvars.String("TELEGRAM_DOWNLOAD_MESSAGE_PATTERN", `Download started{{range .}}\n{{.Anime.Title.UserPreferred}} episode {{.Episode}}{{end}}`))
TelegramDownloadMessagePattern = template.Must(template.New("TELEGRAM_DOWNLOAD_MESSAGE_PATTERN").Parse(TelegramDownloadMessagePatternStr))
DownloadAll = envvars.Bool("DOWNLOAD_ALL_ANIMES", false)
)

12
errors.go Normal file
View File

@ -0,0 +1,12 @@
package main
import "git.tordarus.net/tordarus/adverr/v2"
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')")
ErrTorrentNotObtainable = adverr.NewErrTmpl("ErrTorrentNotObtainable", "torrents from nyaa.si not obtainable (reason: %s)")
ErrDownloadTorrentFileFailed = adverr.NewErrTmpl("ErrDownloadTorrentFileFailed", "torrent file download failed: %s")
ErrLockFileCreationFailed = adverr.NewErrTmpl("ErrLockFileCreationFailed", "creation of lock file for anime '%s' and episode %d failed")
ErrSaveTorrentFileFailed = adverr.NewErrTmpl("ErrSaveTorrentFileFailed", "torrent file could not be saved: %s")
)

78
get_torrents.go Normal file
View File

@ -0,0 +1,78 @@
package main
import (
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"git.tordarus.net/nyaanime/model"
"github.com/PuerkitoBio/goquery"
)
var torrentLinkRegex = regexp.MustCompile(`https:\/\/nyaa\.si\/download\/(\d+?)\.torrent`)
func GetTorrents() ([]model.Torrent, error) {
resp, err := http.Get("https://nyaa.si/?page=rss&f=0&c=1_0")
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
}

33
go.mod Normal file
View File

@ -0,0 +1,33 @@
module git.tordarus.net/nyaanime/downloader
go 1.23.0
toolchain go1.24.3
require (
git.tordarus.net/nyaanime/logic v0.0.1
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/envvars v0.0.0-20250114175450-d73e12b838a5
git.tordarus.net/tordarus/slices v0.0.14
git.tordarus.net/tordarus/tprint v0.0.1
github.com/PuerkitoBio/goquery v1.10.3
github.com/fatih/color v1.18.0
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
)
require (
git.tordarus.net/tordarus/channel v0.1.19 // indirect
git.tordarus.net/tordarus/gmath v0.0.7 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
gopkg.in/vansante/go-ffprobe.v2 v2.2.1 // indirect
)

112
go.sum Normal file
View File

@ -0,0 +1,112 @@
git.tordarus.net/nyaanime/logic v0.0.1 h1:fUa/O9/WgzJhmGFGSuy6gx+Rr2Y+/RLee3qaNO0l1lI=
git.tordarus.net/nyaanime/logic v0.0.1/go.mod h1:+4sKxSzwgCoeZv3lyWB3yFa5CVyi/frmwsTqDf8C3mE=
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/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=

31
local_file_check.go Normal file
View File

@ -0,0 +1,31 @@
package main
import (
"errors"
"os"
"path/filepath"
"git.tordarus.net/nyaanime/logic"
"git.tordarus.net/nyaanime/model"
)
func IsCurrentlyDownloading(animeEp model.AnimeEpisode) bool {
animeEpPath := logic.GetAnimeEpFilepath(animeEp, "lock")
_, err := os.Stat(animeEpPath)
return !errors.Is(err, os.ErrNotExist)
}
func SetCurrentlyDownloading(animeEp model.AnimeEpisode) error {
animeEpPath := logic.GetAnimeEpFilepath(animeEp, "lock")
dir := filepath.Dir(animeEpPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
file, err := os.Create(animeEpPath)
if err != nil {
defer file.Close()
}
return err
}

39
main.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"os/exec"
"time"
"git.tordarus.net/nyaanime/logic"
)
func main() {
// check for ffmpeg in PATH
if _, err := exec.LookPath("ffmpeg"); err != nil {
panic(err) // TODO error handling
}
// check for ffprobe in PATH
if _, err := exec.LookPath("ffprobe"); err != nil {
panic(err) // TODO error handling
}
// get access token once at startup to be sure that an access token is obtainable at all
if _, err := logic.GetAnilistAccessToken(); err != nil {
panic(err) // TODO error handling
}
if err := InitTelegramBot(); err != nil {
panic(err) // TODO error handling
}
logic.PrintPriorityTables()
ticker := time.NewTicker(PollRate)
defer ticker.Stop()
CheckTorrents()
for range ticker.C {
CheckTorrents()
}
}

86
show_debug_info.go Normal file
View File

@ -0,0 +1,86 @@
package main
import (
"fmt"
"strings"
"github.com/fatih/color"
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/tordarus/slices"
"git.tordarus.net/tordarus/tprint"
)
var (
BoldText = color.New(color.Bold)
)
func ShowDebugInfo(
parsedTorrentsByAnimeEp, animeListTorrents, essentialTorrents map[model.AnimeEpisode][]*model.ParsedTorrent,
preferredTorrents map[model.AnimeEpisode]*TorrentPriority,
downloadingEpisodes map[model.AnimeEpisode]struct{},
inCollectionEpisodes map[model.AnimeEpisode]bool,
collectionPreferredTorrents map[*model.ParsedTorrent]bool,
downloadedTorrents map[model.AnimeEpisode]*TorrentPriority) {
for animeEp, parsedTorrents := range parsedTorrentsByAnimeEp {
table := tprint.NewTable("id", "resolution", "languages", "subtitles", "seeders", "leechers", "downloads", "trusted", "group", "evaluation")
for _, torrent := range parsedTorrents {
eval := getEvaluation(
slices.Contains(essentialTorrents[animeEp], torrent),
preferredTorrents[animeEp] != nil && preferredTorrents[animeEp].ParsedTorrent == torrent,
collectionPreferredTorrents[torrent],
downloadedTorrents[animeEp] != nil && downloadedTorrents[animeEp].ParsedTorrent == torrent,
)
table.AddRow(
torrent.Torrent.ID,
torrent.Resolution,
strings.Join(torrent.Languages, ", "),
strings.Join(torrent.Subtitles, ", "),
torrent.Torrent.Seeders,
torrent.Torrent.Leechers,
torrent.Torrent.Downloads,
torrent.Torrent.Trusted,
torrent.Parser.Identity,
eval,
)
}
var epState string
if _, onList := animeListTorrents[animeEp]; onList {
if _, downloading := downloadingEpisodes[animeEp]; downloading {
epState = color.BlueString("CURRENTLY DOWNLOADING")
} else if inCollectionEpisodes[animeEp] {
epState = color.GreenString("IN COLLECTION")
} else {
epState = color.YellowString("ON LIST")
}
} else {
epState = color.RedString("NOT ON LIST")
}
b := new(strings.Builder)
if err := DebugAnimeEpisodePattern.Execute(b, animeEp); err != nil {
panic(err)
}
header := BoldText.Sprintf("%s (%s)", color.MagentaString(b.String()), epState)
fmt.Println(tprint.FormatHeaderTable(header, table))
}
}
func getEvaluation(essential, preferred, collectionPreferred, downloaded bool) string {
if downloaded {
return color.GreenString("DOWNLOAD STARTED")
} else if collectionPreferred {
return color.GreenString("collection preferred")
} else if preferred {
return color.BlueString("torrent preferred")
} else if essential {
return color.YellowString("torrent considered")
} else {
return color.RedString("torrent ignored")
}
}

48
telegram.go Normal file
View File

@ -0,0 +1,48 @@
package main
import (
"strings"
"text/template"
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/tordarus/adverr/v2"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)
var TelegramBot *tgbotapi.BotAPI
func InitTelegramBot() error {
if TelegramBotToken != "" && TelegramChatID != 0 {
bot, err := tgbotapi.NewBotAPI(TelegramBotToken)
if err != nil {
return err
}
TelegramBot = bot
}
return nil
}
func SendTelegramMessage(text string) {
if TelegramBot == nil || strings.TrimSpace(text) == "" {
return
}
msg := tgbotapi.NewMessage(TelegramChatID, text)
_, err := TelegramBot.Send(msg)
if err != nil {
adverr.Println(adverr.Wrap("could not send telegram message", err))
}
}
func SendTelegramAnimeEpMessage(messagePattern *template.Template, animeEps []model.AnimeEpisode) {
if len(animeEps) == 0 {
return
}
b := new(strings.Builder)
if err := messagePattern.Execute(b, animeEps); err != nil {
adverr.Println(adverr.Wrap("could not send telegram message", err))
}
SendTelegramMessage(b.String())
}

70
torrent_filter.go Normal file
View File

@ -0,0 +1,70 @@
package main
import (
"git.tordarus.net/nyaanime/logic"
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/tordarus/anilist"
"git.tordarus.net/tordarus/slices"
)
func FilterTorrentsByAnimeList(allTorrents map[model.AnimeEpisode][]*model.ParsedTorrent, animeList map[anilist.MediaID]*anilist.MediaList) map[model.AnimeEpisode][]*model.ParsedTorrent {
if DownloadAll {
return allTorrents
}
filtered := map[model.AnimeEpisode][]*model.ParsedTorrent{}
for animeEp, torrents := range allTorrents {
if _, ok := animeList[animeEp.Anime.ID]; ok {
filtered[animeEp] = torrents
}
}
return filtered
}
func FilterEssentialTorrents(allTorrents map[model.AnimeEpisode][]*model.ParsedTorrent) map[model.AnimeEpisode][]*model.ParsedTorrent {
filtered := map[model.AnimeEpisode][]*model.ParsedTorrent{}
for animeEpisode, parsedTorrents := range allTorrents {
for _, parsedTorrent := range parsedTorrents {
if HasEssentialProperties(parsedTorrent) {
filtered[animeEpisode] = append(filtered[animeEpisode], parsedTorrent)
}
}
}
return filtered
}
func HasEssentialProperties(torrent *model.ParsedTorrent) bool {
if torrent.Resolution < logic.MinResolution || torrent.Resolution > logic.MaxResolution {
return false
}
if torrent.Torrent.Seeders < logic.MinSeeders || torrent.Torrent.Seeders > logic.MaxSeeders {
return false
}
if torrent.Torrent.Leechers < logic.MinLeechers || torrent.Torrent.Leechers > logic.MaxLeechers {
return false
}
if torrent.Torrent.Downloads < logic.MinDownloads || torrent.Torrent.Downloads > logic.MaxDownloads {
return false
}
if logic.TrustedOnly && !torrent.Torrent.Trusted {
return false
}
for _, essentialLanguage := range logic.EssentialLanguages {
if !slices.Contains(torrent.Languages, essentialLanguage) {
return false
}
}
for _, essentialSubtitle := range logic.EssentialSubtitles {
if !slices.Contains(torrent.Subtitles, essentialSubtitle) {
return false
}
}
return true
}

52
torrent_parse.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"errors"
"fmt"
"os"
"git.tordarus.net/nyaanime/logic"
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/nyaanime/parsers"
"git.tordarus.net/tordarus/adverr/v2"
"github.com/fatih/color"
)
func ParseTorrent(torrent *model.Torrent) (*model.ParsedTorrent, error) {
for _, parser := range parsers.Parsers {
parsedTorrent, ok := parser.TorrentParser(&parser, torrent)
if ok {
anime, err := logic.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, logic.ErrAnimeNotFound) {
fmt.Fprintln(os.Stderr, color.RedString("torrent with ID %s could not be parsed because anime was not found: \"%s\"", torrent.ID, parsedTorrent.OriginalAnimeTitle))
} else if !errors.Is(err, ErrNoSuitableParser) {
adverr.Println(err)
}
continue
}
animeEp := GetAnimeEpisode(parsedTorrent.Anime, parsedTorrent.Episode)
torrentsByAnimeEp[animeEp] = append(torrentsByAnimeEp[animeEp], parsedTorrent)
}
return torrentsByAnimeEp
}

28
torrent_priority.go Normal file
View File

@ -0,0 +1,28 @@
package main
import (
"fmt"
"git.tordarus.net/nyaanime/logic"
"git.tordarus.net/nyaanime/model"
)
type TorrentPriority struct {
ParsedTorrent *model.ParsedTorrent
Priority int
PreferredProperties map[string]int
}
func NewTorrentPriority(torrent *model.ParsedTorrent) *TorrentPriority {
priority, preferredProperties := logic.DeterminePriority(torrent)
return &TorrentPriority{
ParsedTorrent: torrent,
Priority: priority,
PreferredProperties: preferredProperties,
}
}
func (tp TorrentPriority) String() string {
return fmt.Sprintf("%s | priority: %d", tp.ParsedTorrent.String(), tp.Priority)
}

28
torrent_sort.go Normal file
View File

@ -0,0 +1,28 @@
package main
import (
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/tordarus/slices"
)
func GetTorrentsWithMaxPrioByAnimeEp(torrents map[model.AnimeEpisode][]*model.ParsedTorrent) map[model.AnimeEpisode]*TorrentPriority {
torrentsWithPrio := map[model.AnimeEpisode]*TorrentPriority{}
for animeEp, torrentList := range torrents {
torrentPrioList := slices.Map(torrentList, NewTorrentPriority)
var maxPrio *TorrentPriority
for _, torrentPrio := range torrentPrioList {
if maxPrio == nil || torrentPrio.Priority > maxPrio.Priority {
maxPrio = torrentPrio
}
}
if maxPrio != nil {
torrentsWithPrio[animeEp] = maxPrio
}
}
return torrentsWithPrio
}

36
utils.go Normal file
View File

@ -0,0 +1,36 @@
package main
import (
"sort"
"git.tordarus.net/nyaanime/model"
"git.tordarus.net/tordarus/tprint"
)
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 GetAnimeEpisodesSortFunc(s []model.AnimeEpisode) func(i, j int) bool {
return func(i, j int) bool {
if s[i].Anime.ID < s[j].Anime.ID {
return true
} else if s[i].Anime.ID > s[j].Anime.ID {
return false
} else {
return s[i].Episode <= s[j].Episode
}
}
}