Compare commits

..

3 Commits

Author SHA1 Message Date
Timon Ringwald
95f77cf189 check local files for already downloaded torrents and currently downloading torrents 2022-08-22 14:14:14 +02:00
Timon Ringwald
820d45d80d check anime episode on hard drive 2022-08-22 00:13:50 +02:00
Timon Ringwald
0b53777f39 logging improved 2022-08-21 21:36:44 +02:00
12 changed files with 299 additions and 19 deletions

View File

@ -12,4 +12,6 @@ var (
ErrAnimeListNotObtainable = adverr.NewErrTmpl("ErrAnimeListNotObtainable", "anime list from anilist.co not obtainable (reason: %s)") 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)") ErrTorrentNotObtainable = adverr.NewErrTmpl("ErrTorrentNotObtainable", "torrents from nyaa.si not obtainable (reason: %s)")
ErrInvalidAnimeStatus = adverr.NewErrTmpl("ErrInvalidAnimeStatus", "invalid status '%s' in ANIME_STATUS (allowed: %s)") ErrInvalidAnimeStatus = adverr.NewErrTmpl("ErrInvalidAnimeStatus", "invalid status '%s' in ANIME_STATUS (allowed: %s)")
ErrInvalidGlobSyntax = adverr.NewErrTmpl("ErrInvalidGlobSyntax", "invalid filepath.Glob syntax: '%s'")
ErrNoSuitableFileFound = adverr.NewErrTmpl("ErrNoSuitableFileFound", "no file found with essential properties for anime '%s' and episode %d")
) )

25
file_filter.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"git.milar.in/milarin/slices"
)
func HasFileEssentialProperties(props *FileProperties) bool {
if props.Resolution < MinResolution || props.Resolution > MaxResolution {
return false
}
for _, essentialLanguage := range EssentialLanguages {
if !slices.Contains(props.Languages, essentialLanguage) {
return false
}
}
for _, essentialSubtitle := range EssentialSubtitles {
if !slices.Contains(props.Subtitles, essentialSubtitle) {
return false
}
}
return true
}

17
file_priority.go Normal file
View File

@ -0,0 +1,17 @@
package main
type FilePriority struct {
Properties *FileProperties
Priority int
PreferredProperties map[string]int
}
func NewFilePriority(props *FileProperties) *FilePriority {
priority, preferredProperties := DeterminePriority(props)
return &FilePriority{
Properties: props,
Priority: priority,
PreferredProperties: preferredProperties,
}
}

79
file_properties.go Normal file
View File

@ -0,0 +1,79 @@
package main
import (
"context"
"fmt"
"os"
"git.milar.in/nyaanime/model"
"git.milar.in/nyaanime/parsers"
ffprobe "gopkg.in/vansante/go-ffprobe.v2"
)
type FileProperties struct {
Filepath string
Languages []string
Subtitles []string
Resolution model.Resolution
}
var _ model.PropertyHolder = &FileProperties{}
// TODO cache
func AnalyzeFile(path string) (*FileProperties, error) {
props := &FileProperties{Filepath: path}
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
data, err := ffprobe.ProbeReader(context.Background(), file)
if err != nil {
return nil, err
}
defaultVideoLang := ""
for _, s := range data.StreamType(ffprobe.StreamVideo) {
if s.Disposition.Default > 0 {
props.Resolution = model.Resolution(s.Height)
defaultVideoLang = parsers.ParseLanguage(s.Tags.Language)
break
}
}
for _, s := range data.StreamType(ffprobe.StreamAudio) {
if s.Tags.Language != "" {
props.Languages = append(props.Languages, parsers.ParseLanguage(s.Tags.Language))
} else if s.Disposition.Default > 0 {
props.Languages = append(props.Languages, defaultVideoLang)
}
}
for _, s := range data.StreamType(ffprobe.StreamSubtitle) {
if s.Tags.Language != "" {
props.Subtitles = append(props.Subtitles, parsers.ParseLanguage(s.Tags.Language))
} else if s.Disposition.Default > 0 {
props.Subtitles = append(props.Subtitles, defaultVideoLang)
}
}
return props, nil
}
func (fp *FileProperties) GetLanguages() []string {
return fp.Languages
}
func (fp *FileProperties) GetSubtitles() []string {
return fp.Subtitles
}
func (fp *FileProperties) GetResolution() model.Resolution {
return fp.Resolution
}
func (fp *FileProperties) String() string {
return fmt.Sprintf("")
}

5
go.mod
View File

@ -9,9 +9,10 @@ require (
git.milar.in/milarin/envvars v1.0.3 git.milar.in/milarin/envvars v1.0.3
git.milar.in/milarin/gmath v0.0.2 git.milar.in/milarin/gmath v0.0.2
git.milar.in/milarin/slices v0.0.2 git.milar.in/milarin/slices v0.0.2
git.milar.in/nyaanime/model v0.0.0-20220821124037-0a28c6b41556 git.milar.in/nyaanime/model v0.0.0-20220822093541-87208e95e7ac
git.milar.in/nyaanime/parsers v0.0.0-20220815144327-52de61265e27 git.milar.in/nyaanime/parsers v0.0.0-20220822100125-2813a7868f6a
github.com/PuerkitoBio/goquery v1.8.0 github.com/PuerkitoBio/goquery v1.8.0
gopkg.in/vansante/go-ffprobe.v2 v2.1.0
) )
require ( require (

10
go.sum
View File

@ -10,14 +10,18 @@ git.milar.in/milarin/gmath v0.0.2 h1:avz+75f8XqAYA1wEB6kis0R5xvRuepBKTqBuJBjh6Yw
git.milar.in/milarin/gmath v0.0.2/go.mod h1:HDLftG5RLpiNGKiIWh+O2G1PYkNzyLDADO8Cd/1abiE= git.milar.in/milarin/gmath v0.0.2/go.mod h1:HDLftG5RLpiNGKiIWh+O2G1PYkNzyLDADO8Cd/1abiE=
git.milar.in/milarin/slices v0.0.2 h1:j92MuP0HWKSaHJMq/FRxDtSDIGiOTvw+KogUTwuulr0= git.milar.in/milarin/slices v0.0.2 h1:j92MuP0HWKSaHJMq/FRxDtSDIGiOTvw+KogUTwuulr0=
git.milar.in/milarin/slices v0.0.2/go.mod h1:XRNfE99aNKeaPOY1phjOlpIQqeGCW1LOqqh8UHS+vAk= git.milar.in/milarin/slices v0.0.2/go.mod h1:XRNfE99aNKeaPOY1phjOlpIQqeGCW1LOqqh8UHS+vAk=
git.milar.in/nyaanime/model v0.0.0-20220821124037-0a28c6b41556 h1:RplYz4+CMK9ByI3ELusBltWFRlDs6VMOGk5EyENnLi0= git.milar.in/nyaanime/model v0.0.0-20220822093541-87208e95e7ac h1:rM5Mpo4/OJuZaBNZdylag+gi8giWVwDbqsoPjhDP9+g=
git.milar.in/nyaanime/model v0.0.0-20220821124037-0a28c6b41556/go.mod h1:OzhQgj0b/Hf9fg8VXYxYt8ONQOvHm8xC44TmS9kQ150= git.milar.in/nyaanime/model v0.0.0-20220822093541-87208e95e7ac/go.mod h1:OzhQgj0b/Hf9fg8VXYxYt8ONQOvHm8xC44TmS9kQ150=
git.milar.in/nyaanime/parsers v0.0.0-20220815144327-52de61265e27 h1:0+5j9MMJQS8+Luss19hD6hvNFxcBDRal2XwSUTyq7WU= git.milar.in/nyaanime/parsers v0.0.0-20220815144327-52de61265e27 h1:0+5j9MMJQS8+Luss19hD6hvNFxcBDRal2XwSUTyq7WU=
git.milar.in/nyaanime/parsers v0.0.0-20220815144327-52de61265e27/go.mod h1:qm6fIFBFs90uz7IJ8RKgDir0K8Fa8isixGLgrtC6kgU= git.milar.in/nyaanime/parsers v0.0.0-20220815144327-52de61265e27/go.mod h1:qm6fIFBFs90uz7IJ8RKgDir0K8Fa8isixGLgrtC6kgU=
git.milar.in/nyaanime/parsers v0.0.0-20220822100125-2813a7868f6a h1:7vrKOL/vpqJ8YFZ9tmq9iPLoBuLnZgptHWaScyFOFFo=
git.milar.in/nyaanime/parsers v0.0.0-20220822100125-2813a7868f6a/go.mod h1:qm6fIFBFs90uz7IJ8RKgDir0K8Fa8isixGLgrtC6kgU=
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= 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/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 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/vansante/go-ffprobe v1.1.0 h1:Tz5X+38tF8YYEFVz+PUTrtvlED35IorB7XI0USOqZWU=
github.com/vansante/go-ffprobe v1.1.0/go.mod h1:AEIxsTWYTTeXpel90yu5J/QxuDWNaKCO50xRBN4rdac=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E=
golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
@ -26,3 +30,5 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/vansante/go-ffprobe.v2 v2.1.0 h1:Gh8oVkvOSZG/DgEMmBw8h4oLvJhSQHROEGp3TaVGq08=
gopkg.in/vansante/go-ffprobe.v2 v2.1.0/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE=

87
local_file_check.go Normal file
View File

@ -0,0 +1,87 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"git.milar.in/milarin/anilist"
"git.milar.in/nyaanime/model"
)
type AnimePathPatternData struct {
Title anilist.MediaTitle
Episode int
Ext string
}
func GetAnimeEpFilepath(animeEp model.AnimeEpisode, ext string) string {
tmplData := AnimePathPatternData{
Title: animeEp.Anime.Title,
Episode: animeEp.Episode,
Ext: ext,
}
b := new(strings.Builder)
if err := AnimeEpFilepathPattern.Execute(b, tmplData); err != nil {
panic(err)
}
return b.String()
}
func AnimeEpExistsLocally(animeEp model.AnimeEpisode) bool {
animeEpPath := filepath.Join(AnimePath, GetAnimeEpFilepath(animeEp, "*"))
files, err := filepath.Glob(animeEpPath)
if err != nil {
panic(err)
}
return len(files) > 0
}
func GetLocalAnimeEpProperties(animeEp model.AnimeEpisode) (*FilePriority, bool, error) {
animeEpPath := filepath.Join(AnimePath, GetAnimeEpFilepath(animeEp, "*"))
files, err := filepath.Glob(animeEpPath)
if err != nil {
return nil, false, ErrInvalidGlobSyntax.Wrap(err, animeEpPath)
}
var mostPrio *FilePriority
for _, file := range files {
props, err := 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, nil
}
// TODO cache?
func TorrentFileDownloading(torrent *model.ParsedTorrent) bool {
torrentFile := filepath.Join(TorrentPath, fmt.Sprintf("%s.torrent", torrent.Torrent.ID))
if _, err := os.Stat(torrentFile); err == nil {
return true
}
addedTorrentFile := filepath.Join(TorrentPath, fmt.Sprintf("%s.torrent.added", torrent.Torrent.ID))
if _, err := os.Stat(addedTorrentFile); err == nil {
return true
}
return false
}

50
main.go
View File

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"html/template"
"math" "math"
"time" "time"
@ -21,6 +22,15 @@ var (
StorageUser = envvars.String("STORAGE_USER", "") StorageUser = envvars.String("STORAGE_USER", "")
StoragePass = envvars.String("STORAGE_PASS", "") StoragePass = envvars.String("STORAGE_PASS", "")
TorrentPath = envvars.String("TORRENT_PATH", "")
AnimePath = envvars.String("ANIME_PATH", "")
AnimeEpFilepathPattern = envvars.Object(
"EPISODE_FILEPATH_PATTERN",
template.Must(template.New("anime-episode-filepath-pattern").Parse(`{{.Title.UserPreferred}}/{{.Title.UserPreferred}} Episode {{.Episode}}.{{.Ext}}`)),
template.New("anime-episode-filepath-pattern").Parse,
)
AnimeStatus = envvars.ObjectSlice("ANIME_STATUS", ",", ParseMediaListStatus) AnimeStatus = envvars.ObjectSlice("ANIME_STATUS", ",", ParseMediaListStatus)
// essential torrent properties // essential torrent properties
@ -57,9 +67,9 @@ var (
) )
func main() { func main() {
fmt.Println("language priorites:", PreferredLanguages) fmt.Println("language priorites:", Map2Str(PreferredLanguages))
fmt.Println("subtitle priorites:", PreferredSubtitles) fmt.Println("subtitle priorites:", Map2Str(PreferredSubtitles))
fmt.Println("resolution priorites:", PreferredResolutions) fmt.Println("resolution priorites:", Map2Str(PreferredResolutions))
if len(AnimeStatus) == 0 { if len(AnimeStatus) == 0 {
AnimeStatus = []anilist.MediaListStatus{ AnimeStatus = []anilist.MediaListStatus{
@ -92,18 +102,44 @@ func checkTorrents() {
return return
} }
/*animes, err = GetAnimesToDownloadByAnimeID() animes, err := GetAnimesToDownloadByAnimeID()
if err != nil { if err != nil {
fmt.Println(adverr.Wrap("retrieving anime list failed", err)) fmt.Println(adverr.Wrap("retrieving anime list failed", err))
return return
}*/ }
parsedTorrents := ParseTorrentsByAnimeEpSortedByProperties(torrents) parsedTorrents := ParseTorrentsByAnimeEpSortedByProperties(torrents)
for animeEp, torrentPriorities := range parsedTorrents { for animeEp, torrentPriorities := range parsedTorrents {
fmt.Printf("\nanime: %s | episode: %d | torrents found: %d\n", animeEp.Anime, animeEp.Episode, len(torrentPriorities)) _, animeOnList := animes[animeEp.Anime.ID]
props, found, err := GetLocalAnimeEpProperties(animeEp)
episodeInCollection := err == nil && found
fmt.Print(FormatFilePriority(animeEp, props, animeOnList, episodeInCollection, len(torrentPriorities)))
if !animeOnList {
continue
}
for _, torrentPriority := range torrentPriorities { for _, torrentPriority := range torrentPriorities {
PrintTorrentPriority(torrentPriority) if TorrentFileDownloading(torrentPriority.ParsedTorrent) {
fmt.Printf("%s | CURRENTLY DOWNLOADING\n", FormatTorrentPriority(torrentPriority))
continue
}
if episodeInCollection {
if props.Priority > torrentPriority.Priority {
fmt.Printf("%s | LOWER PRIORITY\n", FormatTorrentPriority(torrentPriority))
} else {
fmt.Printf("%s | HIGHER PRIORITY | STARTING DOWNLOAD\n", FormatTorrentPriority(torrentPriority))
// TODO start download
}
} else {
fmt.Printf("%s | NOT IN COLLECTION | STARTING DOWNLOAD\n", FormatTorrentPriority(torrentPriority))
// TODO start download
}
// TODO download anime episode with highest priority (first one in slice)
} }
} }

View File

@ -42,7 +42,7 @@ func ParseTorrentsByAnimeEp(torrents []model.Torrent) map[model.AnimeEpisode][]*
} }
animeEpisode := model.AnimeEpisode{ animeEpisode := model.AnimeEpisode{
Anime: parsedTorrent.Anime.Title.Native, Anime: parsedTorrent.Anime,
Episode: parsedTorrent.Episode, Episode: parsedTorrent.Episode,
} }

View File

@ -19,26 +19,26 @@ func SortTorrentsByPreferredProperties(torrents map[model.AnimeEpisode][]*model.
return torrentsWithPrio return torrentsWithPrio
} }
func DeterminePriority(torrent *model.ParsedTorrent) (priority int, preferredProperties map[string]int) { func DeterminePriority(props model.PropertyHolder) (priority int, preferredProperties map[string]int) {
preferredProperties = map[string]int{} preferredProperties = map[string]int{}
for _, lang := range torrent.Languages { for _, lang := range props.GetLanguages() {
if langPriority, ok := PreferredLanguages[lang]; ok { if langPriority, ok := PreferredLanguages[lang]; ok {
priority += langPriority priority += langPriority
preferredProperties["lang/"+lang] = langPriority preferredProperties["lang/"+lang] = langPriority
} }
} }
for _, sub := range torrent.Subtitles { for _, sub := range props.GetSubtitles() {
if subPriority, ok := PreferredSubtitles[sub]; ok { if subPriority, ok := PreferredSubtitles[sub]; ok {
priority += subPriority priority += subPriority
preferredProperties["sub/"+sub] = subPriority preferredProperties["sub/"+sub] = subPriority
} }
} }
if prefRes, ok := PreferredResolutions[torrent.Resolution]; ok { if prefRes, ok := PreferredResolutions[props.GetResolution()]; ok {
priority += prefRes priority += prefRes
preferredProperties["res/"+torrent.Resolution.String()] = prefRes preferredProperties["res/"+props.GetResolution().String()] = prefRes
} }
return return

View File

@ -6,6 +6,7 @@ import (
"git.milar.in/milarin/anilist" "git.milar.in/milarin/anilist"
"git.milar.in/milarin/slices" "git.milar.in/milarin/slices"
"git.milar.in/nyaanime/model"
) )
var AllMediaListStatuses = []anilist.MediaListStatus{ var AllMediaListStatuses = []anilist.MediaListStatus{
@ -43,8 +44,8 @@ func Map2Str[K comparable, T any](m map[K]T) string {
return str[:len(str)-1] return str[:len(str)-1]
} }
func PrintTorrentPriority(torrentPriority *TorrentPriority) { func FormatTorrentPriority(torrentPriority *TorrentPriority) string {
fmt.Printf("id: %s | resolution: %d | languages: %s | subtitles: %s | seeders: %d | leechers: %d, | downloads: %d | trusted: %t | preferred properties: %s | priority: %d\n", return fmt.Sprintf("id: %s | resolution: %d | languages: %s | subtitles: %s | seeders: %d | leechers: %d, | downloads: %d | trusted: %t | preferred properties: %s | priority: %d",
torrentPriority.ParsedTorrent.Torrent.ID, torrentPriority.ParsedTorrent.Torrent.ID,
torrentPriority.ParsedTorrent.Resolution, torrentPriority.ParsedTorrent.Resolution,
strings.Join(torrentPriority.ParsedTorrent.Languages, ","), strings.Join(torrentPriority.ParsedTorrent.Languages, ","),
@ -57,3 +58,29 @@ func PrintTorrentPriority(torrentPriority *TorrentPriority) {
torrentPriority.Priority, torrentPriority.Priority,
) )
} }
func FormatFilePriority(animeEp model.AnimeEpisode, props *FilePriority, onList, inCollection bool, torrentCount int) string {
if !onList {
return fmt.Sprintf("\nanime: %s | episode: %d | torrents found: %d | NOT ON LIST\n",
animeEp.Anime.Title.Romaji,
animeEp.Episode,
torrentCount,
)
}
if inCollection {
return fmt.Sprintf("\nanime: %s | episode: %d | torrents found: %d | IN COLLECTION | file properties: %s | file priority: %d\n",
animeEp.Anime.Title.Romaji,
animeEp.Episode,
torrentCount,
Map2Str(props.PreferredProperties),
props.Priority,
)
}
return fmt.Sprintf("\nanime: %s | episode: %d | torrents found: %d\n",
animeEp.Anime.Title.Romaji,
animeEp.Episode,
torrentCount,
)
}