initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.env
|
||||
organizer
|
22
bytechan_writer.go
Normal file
22
bytechan_writer.go
Normal file
@ -0,0 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
func NewWriterFromByteChan(ch chan byte) *ByteChanWriter {
|
||||
return &ByteChanWriter{ch}
|
||||
}
|
||||
|
||||
type ByteChanWriter struct {
|
||||
ch chan byte
|
||||
}
|
||||
|
||||
var _ io.Writer = &ByteChanWriter{}
|
||||
|
||||
func (w *ByteChanWriter) Write(p []byte) (n int, err error) {
|
||||
for _, b := range p {
|
||||
w.ch <- b
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
43
encode_video.go
Normal file
43
encode_video.go
Normal file
@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func EncodeVideo(w io.Writer, encArgs, oldFile, newFile string) error {
|
||||
if encArgs == "" {
|
||||
fmt.Fprintf(w, "\trename file\n\t from: '%s'\n\t to: '%s'\n", oldFile, newFile)
|
||||
return os.Rename(oldFile, newFile)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
fmt.Fprintf(w, "\tencode file\n\t from: '%s'\n\t to: '%s'\n", oldFile, newFile)
|
||||
|
||||
fullArgs := []string{"-y", "-i", oldFile}
|
||||
fullArgs = append(fullArgs, strings.Split(encArgs, " ")...)
|
||||
fullArgs = append(fullArgs, newFile)
|
||||
|
||||
cmd := exec.Command("ffmpeg", fullArgs...)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Remove(oldFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\t done (took %s)\n", time.Since(start).Truncate(100*time.Millisecond))
|
||||
|
||||
return nil
|
||||
}
|
52
envvars.go
Normal file
52
envvars.go
Normal file
@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"git.tordarus.net/nyaanime/logic"
|
||||
|
||||
"git.tordarus.net/tordarus/envvars"
|
||||
)
|
||||
|
||||
var (
|
||||
DownloadPath = envvars.String("DOWNLOAD_PATH", "")
|
||||
ThreadCount = envvars.Int("THREADS", runtime.NumCPU())
|
||||
DeleteLowPriorityFiles = envvars.Bool("DELETE_LOW_PRIORITY_FILES", false)
|
||||
|
||||
TelegramBotToken = envvars.String("TELEGRAM_API_TOKEN", "")
|
||||
TelegramChatID = envvars.Int64("TELEGRAM_CHAT_ID", 0)
|
||||
TelegramOrganizeMessagePatternStr = logic.EscSeqReplacer.Replace(envvars.String("TELEGRAM_ORGANIZE_MESSAGE_PATTERN", `<u><b>Recently Downloaded Animes ({{len .}} eps)</b></u>{{range .}}\n{{.Anime.Title.UserPreferred}} Episode {{.Episode}}{{end}}`))
|
||||
TelegramOrganizeMessagePattern = template.Must(template.New("TELEGRAM_ORGANIZE_MESSAGE_PATTERN").Parse(TelegramOrganizeMessagePatternStr))
|
||||
TelegramOrganizeMessageSendCondition = envvars.ObjectSlice("TELEGRAM_ORGANIZE_MESSAGE_SEND_CONDITION", ",", []SendCondition{SendConditionAlways}, SendConditionFromString)
|
||||
TelegramOrganizeMessageSendInterval = envvars.Duration("TELEGRAM_ORGANIZE_MESSAGE_SEND_INTERVAL", 0)
|
||||
TelegramOrganizeMessageSendOffset = envvars.Duration("TELEGRAM_ORGANIZE_MESSAGE_SEND_OFFSET", 0)
|
||||
|
||||
Uid = envvars.Object("UID", 1000, func(s string) (int, error) {
|
||||
if uid, err := strconv.Atoi(s); err == nil {
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
usr, err := user.Lookup(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.Atoi(usr.Uid)
|
||||
})
|
||||
|
||||
Gid = envvars.Object("GID", 1000, func(s string) (int, error) {
|
||||
if gid, err := strconv.Atoi(s); err == nil {
|
||||
return gid, nil
|
||||
}
|
||||
|
||||
grp, err := user.LookupGroup(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.Atoi(grp.Gid)
|
||||
})
|
||||
)
|
17
file_priority_string.go
Normal file
17
file_priority_string.go
Normal file
@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.tordarus.net/nyaanime/logic"
|
||||
)
|
||||
|
||||
func FilePrio2Str(fp *logic.FilePriority) string {
|
||||
return fmt.Sprintf("priority: %d | resolution: %s | languages: %s | subtitles: %s",
|
||||
fp.Priority,
|
||||
fp.Properties.GetResolution().String(),
|
||||
strings.Join(fp.Properties.GetLanguages(), ", "),
|
||||
strings.Join(fp.Properties.GetSubtitles(), ", "),
|
||||
)
|
||||
}
|
25
filehandle.go
Normal file
25
filehandle.go
Normal file
@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type FileHandle struct {
|
||||
File string
|
||||
Chan chan byte
|
||||
Writer io.Writer
|
||||
}
|
||||
|
||||
func (fh *FileHandle) Close() error {
|
||||
close(fh.Chan)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewFileHandle(path string) *FileHandle {
|
||||
out := make(chan byte, 4096)
|
||||
return &FileHandle{
|
||||
File: path,
|
||||
Chan: out,
|
||||
Writer: NewWriterFromByteChan(out),
|
||||
}
|
||||
}
|
31
go.mod
Normal file
31
go.mod
Normal file
@ -0,0 +1,31 @@
|
||||
module git.tordarus.net/nyaanime/organizer
|
||||
|
||||
go 1.23
|
||||
|
||||
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/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
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||
)
|
||||
|
||||
require (
|
||||
git.tordarus.net/tordarus/tprint v0.0.1 // 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/sys v0.25.0 // indirect
|
||||
gopkg.in/vansante/go-ffprobe.v2 v2.2.1 // indirect
|
||||
)
|
43
go.sum
Normal file
43
go.sum
Normal file
@ -0,0 +1,43 @@
|
||||
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
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/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=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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=
|
168
handle_file.go
Normal file
168
handle_file.go
Normal file
@ -0,0 +1,168 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"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 HandleFile(fh *FileHandle) {
|
||||
defer fh.Close()
|
||||
path := fh.File
|
||||
w := fh.Writer
|
||||
|
||||
fmt.Fprint(w, color.MagentaString("%s file found: %s\n", time.Now().Format("2006-01-02 15:04:05"), path))
|
||||
|
||||
fmt.Fprint(w, "\ttry parsers: ")
|
||||
for i, parser := range parsers.Parsers {
|
||||
parsedFile, ok := parser.FileParser(&parser, path)
|
||||
if !ok {
|
||||
fmt.Fprint(w, color.YellowString(parser.Identity))
|
||||
if i < len(parsers.Parsers)-1 {
|
||||
fmt.Fprint(w, ", ")
|
||||
} else {
|
||||
fmt.Fprintln(w, color.RedString("\n\tfile ignored"))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
anime, err := logic.SearchAnimeByTitle(parsedFile.OriginalAnimeTitle)
|
||||
if err != nil {
|
||||
fmt.Fprintln(w, color.RedString(parser.Identity))
|
||||
fmt.Fprintln(w, color.RedString("\tanime not found: '%s'", parsedFile.OriginalAnimeTitle))
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, color.GreenString(parser.Identity))
|
||||
|
||||
parsedFile.Anime = anime
|
||||
HandleParsedFile(w, parsedFile)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func HandleParsedFile(w io.Writer, parsedFile *model.ParsedFile) {
|
||||
newFilePrio := logic.NewFilePriority(parsedFile)
|
||||
oldFilePrio, animeEpNotExistLocally := logic.GetAnimeEpProps(parsedFile.AnimeEpisode())
|
||||
|
||||
// debug output
|
||||
if animeEpNotExistLocally {
|
||||
fmt.Fprintln(w, "\tfile exists locally")
|
||||
fmt.Fprintf(w, "\t local file: %s\n", FilePrio2Str(oldFilePrio))
|
||||
fmt.Fprintf(w, "\t new file: %s\n", FilePrio2Str(newFilePrio))
|
||||
if newFilePrio.Priority > oldFilePrio.Priority {
|
||||
fmt.Fprint(w, color.GreenString("\t overwrite local file\n"))
|
||||
} else if !DeleteLowPriorityFiles {
|
||||
fmt.Fprint(w, color.YellowString("\t ignore new file\n"))
|
||||
}
|
||||
}
|
||||
|
||||
// delete files with lower priority from DownloadPath
|
||||
if DeleteLowPriorityFiles && animeEpNotExistLocally && oldFilePrio.Priority >= newFilePrio.Priority {
|
||||
fmt.Fprint(w, color.YellowString("\tdelete file with lower priority '%s'", parsedFile.File))
|
||||
err := os.Remove(parsedFile.File)
|
||||
if err != nil {
|
||||
fmt.Fprint(w, color.RedString(" failed: '%s'\n", err.Error()))
|
||||
} else {
|
||||
fmt.Fprint(w, color.GreenString(" done\n"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !animeEpNotExistLocally || newFilePrio.Priority > oldFilePrio.Priority {
|
||||
DeleteOldAnimeEpisode(parsedFile.AnimeEpisode())
|
||||
if err := OrganizeAnimeEpisode(w, parsedFile); err != nil {
|
||||
fmt.Fprint(w, color.RedString("\terror: %s\n", err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func OrganizeAnimeEpisode(w io.Writer, parsedFile *model.ParsedFile) error {
|
||||
PrepareTelegramAnimeEpMessage(parsedFile.AnimeEpisode())
|
||||
|
||||
start := time.Now()
|
||||
|
||||
oldFile := parsedFile.File
|
||||
newFile := logic.GetAnimeEpFilepath(parsedFile.AnimeEpisode(), "part")
|
||||
encodedFile := logic.GetAnimeEpFilepath(parsedFile.AnimeEpisode(), filepath.Ext(parsedFile.File))
|
||||
lockFile := logic.GetAnimeEpFilepath(parsedFile.AnimeEpisode(), "lock")
|
||||
|
||||
fmt.Fprintf(w, "\tmove file\n\t from: '%s'\n\t to: '%s'\n", oldFile, newFile)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(newFile), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Chown(filepath.Dir(newFile), Uid, Gid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(newFile); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
inputFile, err := os.Open(oldFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer inputFile.Close()
|
||||
|
||||
outputFile, err := os.Create(newFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outputFile.Close()
|
||||
|
||||
if err := os.Chown(newFile, Uid, Gid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
written, err := io.Copy(outputFile, inputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\t done (copied %s in %s)\n", FormatBytes(written), time.Since(start).Truncate(100*time.Millisecond))
|
||||
|
||||
// TODO Video encoding should be done before comparing priorities. merge in downloader repo?
|
||||
if err := EncodeVideo(w, parsedFile.Parser.FileEncoding, newFile, encodedFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Remove(oldFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Remove(lockFile); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteOldAnimeEpisode(animeEp model.AnimeEpisode) {
|
||||
animeEpPath := logic.GetAnimeEpFilepath(animeEp, "*")
|
||||
files, err := filepath.Glob(animeEpPath)
|
||||
if err != nil {
|
||||
panic(logic.ErrInvalidGlobSyntax.Wrap(err, animeEpPath))
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if filepath.Ext(file) == ".lock" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.Remove(file); err != nil {
|
||||
adverr.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
73
main.go
Normal file
73
main.go
Normal file
@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"git.tordarus.net/nyaanime/logic"
|
||||
"git.tordarus.net/tordarus/channel"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
var (
|
||||
// AppCtx notifies all threads to finish their work and shutdown
|
||||
AppCtx, cancelAppCtx = context.WithCancel(context.Background())
|
||||
// AppExitWg is waiting for all threads until they are done
|
||||
AppExitWg = &sync.WaitGroup{}
|
||||
|
||||
Runner = InitializeRunner()
|
||||
)
|
||||
|
||||
func main() {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT)
|
||||
go func() {
|
||||
<-signalChan
|
||||
exit()
|
||||
}()
|
||||
|
||||
// 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()
|
||||
|
||||
fileChan, err := WatchDirectory(fsnotify.Create, DownloadPath)
|
||||
if err != nil {
|
||||
panic(err) // TODO error handling
|
||||
}
|
||||
|
||||
fileHandleChan := channel.Map(fileChan, NewFileHandle)
|
||||
workChan, logChan := channel.Tee(fileHandleChan)
|
||||
|
||||
AppExitWg.Add(1)
|
||||
go func() {
|
||||
defer AppExitWg.Done()
|
||||
channel.Each(workChan, HandleFileInRunner)
|
||||
}()
|
||||
|
||||
channel.Each(logChan, PrintFileHandle)
|
||||
}
|
||||
|
||||
func HandleFileInRunner(fh *FileHandle) {
|
||||
Runner.Run(func() { HandleFile(fh) })
|
||||
}
|
||||
|
||||
func exit() {
|
||||
cancelAppCtx() // notify all threads to shutdown
|
||||
AppExitWg.Wait() // wait for threads until shutdown
|
||||
}
|
45
send_condition.go
Normal file
45
send_condition.go
Normal file
@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"git.tordarus.net/nyaanime/model"
|
||||
"git.tordarus.net/tordarus/anilist"
|
||||
)
|
||||
|
||||
type SendCondition string
|
||||
|
||||
var AllSendConditions = []SendCondition{SendConditionOnList, SendConditionAlways, SendConditionPlanned, SendConditionNotWatched}
|
||||
|
||||
const (
|
||||
SendConditionOnList = "ON_LIST"
|
||||
SendConditionAlways = "ALWAYS"
|
||||
SendConditionPlanned = "PLANNED"
|
||||
SendConditionNotWatched = "NOT_WATCHED"
|
||||
)
|
||||
|
||||
func SendConditionFromString(str string) (SendCondition, error) {
|
||||
str = strings.ToLower(str)
|
||||
for _, condition := range AllSendConditions {
|
||||
if str == strings.ToLower(string(condition)) {
|
||||
return condition, nil
|
||||
}
|
||||
}
|
||||
return SendCondition(""), errors.New("invalid message send condition")
|
||||
}
|
||||
|
||||
func (c SendCondition) ShouldSend(animeEp model.AnimeEpisode, listEntry *anilist.MediaList) bool {
|
||||
switch c {
|
||||
case SendConditionOnList:
|
||||
return listEntry != nil
|
||||
case SendConditionPlanned:
|
||||
return listEntry != nil && listEntry.Status == anilist.MediaListStatusPlanning
|
||||
case SendConditionNotWatched:
|
||||
return listEntry != nil && listEntry.Progress < animeEp.Episode
|
||||
case SendConditionAlways:
|
||||
return true
|
||||
default:
|
||||
panic("invalid telegram message send condition")
|
||||
}
|
||||
}
|
165
telegram.go
Normal file
165
telegram.go
Normal file
@ -0,0 +1,165 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.tordarus.net/nyaanime/logic"
|
||||
"git.tordarus.net/nyaanime/model"
|
||||
"git.tordarus.net/tordarus/adverr/v2"
|
||||
"git.tordarus.net/tordarus/anilist"
|
||||
"git.tordarus.net/tordarus/channel"
|
||||
"git.tordarus.net/tordarus/slices"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
)
|
||||
|
||||
var TelegramBot *tgbotapi.BotAPI
|
||||
var AnimeEpisodeMessageChannel = make(chan model.AnimeEpisode, 1000)
|
||||
|
||||
func InitTelegramBot() error {
|
||||
if TelegramBotToken != "" && TelegramChatID != 0 {
|
||||
bot, err := tgbotapi.NewBotAPI(TelegramBotToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
TelegramBot = bot
|
||||
}
|
||||
|
||||
// close channel on app shutdown
|
||||
go func() {
|
||||
<-AppCtx.Done()
|
||||
close(AnimeEpisodeMessageChannel)
|
||||
}()
|
||||
|
||||
AppExitWg.Add(1)
|
||||
go SendMessagePeriodically()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendMessagePeriodically() {
|
||||
defer AppExitWg.Done()
|
||||
|
||||
var messagesPerInterval <-chan []model.AnimeEpisode
|
||||
|
||||
if TelegramOrganizeMessageSendInterval > 0 {
|
||||
WaitForNextTelegramSendCycle()
|
||||
sendAllQueuedAnimeEpisodes()
|
||||
|
||||
grouperFunc := func(current []model.AnimeEpisode, value model.AnimeEpisode) []model.AnimeEpisode {
|
||||
return append(current, value)
|
||||
}
|
||||
messagesPerInterval = channel.GroupByTime(AnimeEpisodeMessageChannel, TelegramOrganizeMessageSendInterval, grouperFunc)
|
||||
} else {
|
||||
mapperFunc := func(value model.AnimeEpisode) []model.AnimeEpisode {
|
||||
return []model.AnimeEpisode{value}
|
||||
}
|
||||
messagesPerInterval = channel.MapSuccessive(AnimeEpisodeMessageChannel, mapperFunc)
|
||||
}
|
||||
|
||||
for animeEpisodes := range messagesPerInterval {
|
||||
sendTelegramAnimeEpMessage(animeEpisodes)
|
||||
}
|
||||
}
|
||||
|
||||
func sendAllQueuedAnimeEpisodes() {
|
||||
queuedEpisodeAmount := len(AnimeEpisodeMessageChannel)
|
||||
animeEpisodes := make([]model.AnimeEpisode, 0, queuedEpisodeAmount)
|
||||
for i := 0; i < queuedEpisodeAmount; i++ {
|
||||
animeEpisodes = append(animeEpisodes, <-AnimeEpisodeMessageChannel)
|
||||
}
|
||||
sendTelegramAnimeEpMessage(animeEpisodes)
|
||||
}
|
||||
|
||||
func SendTelegramMessage(text string) {
|
||||
if TelegramBot == nil || strings.TrimSpace(text) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
msg := tgbotapi.NewMessage(TelegramChatID, text)
|
||||
msg.ParseMode = "html"
|
||||
_, err := TelegramBot.Send(msg)
|
||||
if err != nil {
|
||||
adverr.Println(adverr.Wrap("could not send telegram message", err))
|
||||
}
|
||||
}
|
||||
|
||||
func PrepareTelegramAnimeEpMessage(animeEp model.AnimeEpisode) {
|
||||
shouldSendMessage, err := CheckSendConditions(animeEp)
|
||||
if err != nil {
|
||||
adverr.Println(adverr.Wrap("could not check telegram message send conditions", err))
|
||||
}
|
||||
|
||||
if !shouldSendMessage {
|
||||
return
|
||||
}
|
||||
|
||||
AnimeEpisodeMessageChannel <- animeEp
|
||||
}
|
||||
|
||||
func sendTelegramAnimeEpMessage(animeEpisodes []model.AnimeEpisode) {
|
||||
if len(animeEpisodes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
animeEpisodes = RemoveDuplicates(animeEpisodes)
|
||||
|
||||
b := new(strings.Builder)
|
||||
if err := TelegramOrganizeMessagePattern.Execute(b, animeEpisodes); err != nil {
|
||||
adverr.Println(adverr.Wrap("could not send telegram message", err))
|
||||
}
|
||||
|
||||
SendTelegramMessage(b.String())
|
||||
}
|
||||
|
||||
func RemoveDuplicates(animeEpisodes []model.AnimeEpisode) []model.AnimeEpisode {
|
||||
mapperFunc := func(animeEp model.AnimeEpisode) (model.Pair[anilist.MediaID, int], model.AnimeEpisode) {
|
||||
return model.Pair[anilist.MediaID, int]{First: animeEp.Anime.ID, Second: animeEp.Episode}, animeEp
|
||||
}
|
||||
|
||||
unmapperFunc := func(key model.Pair[anilist.MediaID, int], value model.AnimeEpisode) model.AnimeEpisode {
|
||||
return value
|
||||
}
|
||||
|
||||
return slices.OfMap(slices.ToMap(animeEpisodes, mapperFunc), unmapperFunc)
|
||||
}
|
||||
|
||||
func CheckSendConditions(animeEp model.AnimeEpisode) (bool, error) {
|
||||
listEntry, err := GetListEntry(animeEp.Anime)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, sendCondition := range AllSendConditions {
|
||||
// check if user configured current sendCondition
|
||||
if !slices.Contains(TelegramOrganizeMessageSendCondition, sendCondition) {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if current sendCondition applies for given anime episode
|
||||
if sendCondition.ShouldSend(animeEp, listEntry) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func GetListEntry(anime *anilist.Media) (*anilist.MediaList, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
listEntries, err := logic.GetCurrentlyWatchingAnimesContext(ctx, logic.AllMediaListStatuses...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filteredListEntries := channel.Filter(listEntries, func(a *anilist.MediaList) bool { return a.MediaID == anime.ID })
|
||||
listEntry := channel.FindFirstAndCancelFlush(filteredListEntries, cancel) // TODO flush working properly?
|
||||
|
||||
if listEntry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return *listEntry, nil
|
||||
}
|
65
utils.go
Normal file
65
utils.go
Normal file
@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.tordarus.net/tordarus/channel"
|
||||
"git.tordarus.net/tordarus/gmath"
|
||||
)
|
||||
|
||||
func InitializeRunner() channel.Runner {
|
||||
if ThreadCount <= 0 {
|
||||
return channel.NewUnlimitedRunner()
|
||||
}
|
||||
return channel.NewLimitedRunner(ThreadCount)
|
||||
}
|
||||
|
||||
func FormatBytes[T gmath.Integer](bytes T) string {
|
||||
value := float64(bytes)
|
||||
|
||||
if value >= 1000000000000 {
|
||||
return fmt.Sprintf("%.02fT", value/1000000000000)
|
||||
} else if value >= 1000000000 {
|
||||
return fmt.Sprintf("%.02fG", value/1000000000)
|
||||
} else if value >= 1000000 {
|
||||
return fmt.Sprintf("%.02fM", value/1000000)
|
||||
} else if value >= 1000 {
|
||||
return fmt.Sprintf("%.02fK", value/1000)
|
||||
} else {
|
||||
return fmt.Sprintf("%.02fB", value)
|
||||
}
|
||||
}
|
||||
|
||||
func PrintByteChan(w io.Writer, ch <-chan byte) error {
|
||||
for b := range ch {
|
||||
if _, err := w.Write([]byte{b}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintln(w)
|
||||
return nil
|
||||
}
|
||||
|
||||
func PrintFileHandle(fh *FileHandle) {
|
||||
if err := PrintByteChan(os.Stdout, fh.Chan); err != nil {
|
||||
panic(err) // TODO error handling
|
||||
}
|
||||
}
|
||||
|
||||
func WaitForNextTelegramSendCycle() {
|
||||
now := time.Now()
|
||||
_, offset := now.Zone()
|
||||
offsetDuration := time.Duration(offset) * time.Second
|
||||
lastCycle := now.Truncate(TelegramOrganizeMessageSendInterval).Add(-offsetDuration).Add(TelegramOrganizeMessageSendOffset)
|
||||
|
||||
if durationUntilLastCycle := time.Until(lastCycle); durationUntilLastCycle > 0 {
|
||||
time.Sleep(durationUntilLastCycle)
|
||||
} else {
|
||||
nextCycle := lastCycle.Add(TelegramOrganizeMessageSendInterval)
|
||||
time.Sleep(time.Until(nextCycle))
|
||||
}
|
||||
}
|
95
watch_directory.go
Normal file
95
watch_directory.go
Normal file
@ -0,0 +1,95 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
func WatchDirectory(op fsnotify.Op, path string) (<-chan string, error) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
panic(err) // TODO error handling
|
||||
}
|
||||
|
||||
err = watcher.Add(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ch := make(chan string, 10)
|
||||
|
||||
// close channel on app shutdown
|
||||
go func() {
|
||||
<-AppCtx.Done()
|
||||
watcher.Close()
|
||||
}()
|
||||
|
||||
AppExitWg.Add(1)
|
||||
go func(watcher *fsnotify.Watcher, ch chan<- string) {
|
||||
defer AppExitWg.Done()
|
||||
defer watcher.Close()
|
||||
|
||||
for _, file := range files {
|
||||
path := filepath.Join(path, file.Name())
|
||||
if file.IsDir() {
|
||||
//fmt.Println("watching directory", path)
|
||||
watcher.Add(path)
|
||||
|
||||
if dirFiles, err := os.ReadDir(path); err == nil {
|
||||
for _, dirFile := range dirFiles {
|
||||
ch <- filepath.Join(path, dirFile.Name())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ch <- path
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
|
||||
if fi, err := os.Stat(event.Name); err == nil && fi.IsDir() {
|
||||
if event.Op&fsnotify.Create == fsnotify.Create {
|
||||
//fmt.Println("watching directory", event.Name)
|
||||
watcher.Add(event.Name)
|
||||
}
|
||||
|
||||
// read dir immediately because directory files could simultanously with its parent directory
|
||||
if dirFiles, err := os.ReadDir(event.Name); err == nil {
|
||||
for _, dirFile := range dirFiles {
|
||||
ch <- filepath.Join(event.Name, dirFile.Name())
|
||||
}
|
||||
}
|
||||
} else if err != nil && event.Op&fsnotify.Remove == fsnotify.Remove {
|
||||
//fmt.Println("stopped watching directory", event.Name)
|
||||
watcher.Remove(event.Name)
|
||||
}
|
||||
|
||||
if event.Op&op == op {
|
||||
ch <- event.Name
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if ok {
|
||||
fmt.Println(err, ok)
|
||||
}
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(watcher, ch)
|
||||
|
||||
return ch, nil
|
||||
}
|
Reference in New Issue
Block a user