Initial commit
This commit is contained in:
commit
f1191196d5
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*.log
|
||||||
|
*.txt
|
||||||
|
anitoru
|
||||||
14
anitoru.service
Normal file
14
anitoru.service
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=AniToru System Daemon
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=anitoru
|
||||||
|
Group=anitoru
|
||||||
|
ExecStart=/usr/bin/anitoru -daemon -config=/path/to/config.yml
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
199
cli.go
Normal file
199
cli.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
e "git.kjao.me/kjao/anitoru/err"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubscriptionManager struct {
|
||||||
|
Subscriptions []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubscriptionManager() *SubscriptionManager {
|
||||||
|
var m SubscriptionManager
|
||||||
|
m.Get()
|
||||||
|
return &m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SubscriptionManager) UpdateSubs(conn net.Conn) {
|
||||||
|
rawData, err := ReadData(conn)
|
||||||
|
e.ExitIf(err, "Unable to read from socket.")
|
||||||
|
m.Subscriptions = strings.Split(string(rawData), "\n")
|
||||||
|
if m.Subscriptions[0] == "" {
|
||||||
|
m.Subscriptions = []string{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SubscriptionManager) Get() {
|
||||||
|
conn, err := net.Dial("unix", SocketPath)
|
||||||
|
e.ExitIf(err, "Unable to open socket. Is the daemon running?")
|
||||||
|
|
||||||
|
err = SendData(conn, []byte{'V'})
|
||||||
|
e.ExitIf(err, "Unable to write to socket.")
|
||||||
|
m.UpdateSubs(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SubscriptionManager) View() {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Current subscriptions:")
|
||||||
|
for i, sub := range m.Subscriptions {
|
||||||
|
fmt.Println(fmt.Sprintf("\t[%02d] %v", i+1, sub))
|
||||||
|
}
|
||||||
|
if len(m.Subscriptions) == 0 {
|
||||||
|
fmt.Println("\t...Empty\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SubscriptionManager) Add() {
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Get available subscriptions.
|
||||||
|
fmt.Println("Available subscriptions:")
|
||||||
|
titles, err := GetReleaseSchedule()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Unable to get release schedule.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subMap := GetSubMap(&m.Subscriptions)
|
||||||
|
|
||||||
|
var available []string
|
||||||
|
i := 0
|
||||||
|
for _, title := range titles {
|
||||||
|
_, exists := subMap[title]
|
||||||
|
if !exists {
|
||||||
|
i++
|
||||||
|
available = append(available, title)
|
||||||
|
fmt.Println(fmt.Sprintf("\t[%02d] %v", i, title))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Get comma-separated list of additions.
|
||||||
|
newSubs := []string{}
|
||||||
|
var add string
|
||||||
|
var name string
|
||||||
|
for {
|
||||||
|
fmt.Print("Add subscriptions. (M for manual): ")
|
||||||
|
fmt.Scan(&add)
|
||||||
|
add = strings.ToLower(add)
|
||||||
|
if add == "m" {
|
||||||
|
fmt.Print("Name: ")
|
||||||
|
fmt.Scan(&name)
|
||||||
|
newSubs = append(newSubs, name)
|
||||||
|
} else {
|
||||||
|
addSplit := strings.Split(add, ",")
|
||||||
|
retry := false
|
||||||
|
for _, indexString := range addSplit {
|
||||||
|
index, err := strconv.Atoi(indexString)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error in indices. Try again.")
|
||||||
|
retry = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if index <= 0 || index > len(available) {
|
||||||
|
fmt.Fprintln(os.Stdout, "Error in indices. %v is invalid. Try again", index)
|
||||||
|
retry = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
newSubs = append(newSubs, available[index-1])
|
||||||
|
}
|
||||||
|
if retry {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send addition update to socket
|
||||||
|
data := []byte{'A'}
|
||||||
|
data = append(data, []byte(strings.Join(newSubs, "\n"))...)
|
||||||
|
conn, err := net.Dial("unix", SocketPath)
|
||||||
|
e.ExitIf(err, "Unable to open socket. Is the daemon running?")
|
||||||
|
|
||||||
|
err = SendData(conn, data)
|
||||||
|
e.ExitIf(err, "Unable to write to socket. Is the daemon running?")
|
||||||
|
m.UpdateSubs(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SubscriptionManager) Remove() {
|
||||||
|
fmt.Println()
|
||||||
|
m.View()
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
subMap := GetSubMap(&m.Subscriptions)
|
||||||
|
|
||||||
|
// Get comma-separated list of removals.
|
||||||
|
var remove string
|
||||||
|
removeSubs := []string{}
|
||||||
|
for {
|
||||||
|
fmt.Print("Remove subscriptions: ")
|
||||||
|
fmt.Scan(&remove)
|
||||||
|
addSplit := strings.Split(remove, ",")
|
||||||
|
skip := false
|
||||||
|
for _, indexString := range addSplit {
|
||||||
|
index, err := strconv.Atoi(indexString)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error in indices. Try again.")
|
||||||
|
skip = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if index <= 0 || index > len(m.Subscriptions) {
|
||||||
|
fmt.Fprintln(os.Stdout, "Error in indices. %v is invalid. Try again", index)
|
||||||
|
skip = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
removeSubs = append(removeSubs, (m.Subscriptions)[index-1])
|
||||||
|
delete(subMap, (m.Subscriptions)[index-1])
|
||||||
|
}
|
||||||
|
if skip {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send removal update to socket
|
||||||
|
data := []byte{'R'}
|
||||||
|
data = append(data, []byte(strings.Join(removeSubs, "\n"))...)
|
||||||
|
conn, err := net.Dial("unix", SocketPath)
|
||||||
|
e.ExitIf(err, "Unable to open socket. Is the daemon running?")
|
||||||
|
|
||||||
|
err = SendData(conn, data)
|
||||||
|
e.ExitIf(err, "Unable to write to socket. Is the daemon running?")
|
||||||
|
m.UpdateSubs(conn)
|
||||||
|
m.View()
|
||||||
|
}
|
||||||
|
|
||||||
|
func userCLI() {
|
||||||
|
manager := NewSubscriptionManager()
|
||||||
|
|
||||||
|
var action string
|
||||||
|
for {
|
||||||
|
fmt.Print("View, Add, Remove subscriptions or Quit: ")
|
||||||
|
fmt.Scan(&action)
|
||||||
|
action = strings.ToLower(action)
|
||||||
|
switch action {
|
||||||
|
case "v":
|
||||||
|
manager.View()
|
||||||
|
break
|
||||||
|
case "a":
|
||||||
|
manager.Add()
|
||||||
|
break
|
||||||
|
case "r":
|
||||||
|
manager.Remove()
|
||||||
|
break
|
||||||
|
case "q":
|
||||||
|
os.Exit(1)
|
||||||
|
default:
|
||||||
|
fmt.Println("Invalid command, try again.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
err/err.go
Normal file
68
err/err.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package err
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogOption func(string)
|
||||||
|
|
||||||
|
type ErrorCallback struct {
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func If(err error) *ErrorCallback {
|
||||||
|
return &ErrorCallback{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *ErrorCallback) ThenIf(f func() error) *ErrorCallback {
|
||||||
|
if err.Error == nil {
|
||||||
|
err.Error = f()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *ErrorCallback) Then(f func()) *ErrorCallback {
|
||||||
|
if err.Error == nil {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *ErrorCallback) End() error {
|
||||||
|
return err.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func Panic(err error) bool {
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogIf(err error, opts ...LogOption) bool {
|
||||||
|
if err != nil {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(err.Error())
|
||||||
|
}
|
||||||
|
log.Println(err.Error())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Log(msg string, opts ...LogOption) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(msg)
|
||||||
|
}
|
||||||
|
log.Println(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExitIf(err error, msg string) {
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, msg, "Error:", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
34
go.mod
Normal file
34
go.mod
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
module git.kjao.me/kjao/anitoru
|
||||||
|
|
||||||
|
go 1.22.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/PuerkitoBio/goquery v1.9.2
|
||||||
|
github.com/gookit/config/v2 v2.2.5
|
||||||
|
github.com/mmcdole/gofeed v1.3.0
|
||||||
|
github.com/mrobinsn/go-rtorrent v1.8.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
|
github.com/fatih/color v1.14.1 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.11.2 // indirect
|
||||||
|
github.com/gookit/color v1.5.4 // indirect
|
||||||
|
github.com/gookit/goutil v0.6.15 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/net v0.24.0 // indirect
|
||||||
|
golang.org/x/sync v0.5.0 // indirect
|
||||||
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
|
golang.org/x/term v0.19.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
|
)
|
||||||
103
go.sum
Normal file
103
go.sum
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||||
|
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||||
|
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||||
|
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
|
||||||
|
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||||
|
github.com/goccy/go-yaml v1.11.2 h1:joq77SxuyIs9zzxEjgyLBugMQ9NEgTWxXfz2wVqwAaQ=
|
||||||
|
github.com/goccy/go-yaml v1.11.2/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||||
|
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||||
|
github.com/gookit/config/v2 v2.2.5 h1:RECbYYbtherywmzn3LNeu9NA5ZqhD7MSKEMsJ7l+MpU=
|
||||||
|
github.com/gookit/config/v2 v2.2.5/go.mod h1:NeX+yiNYn6Ei10eJvCQFXuHEPIE/IPS8bqaFIsszzaM=
|
||||||
|
github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo=
|
||||||
|
github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
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.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||||
|
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
|
||||||
|
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
|
||||||
|
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk=
|
||||||
|
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/mrobinsn/go-rtorrent v1.8.0 h1:+61aDIP0asy57lRD/uZtmxfE0/gjkHnt3uddOhMKUJ8=
|
||||||
|
github.com/mrobinsn/go-rtorrent v1.8.0/go.mod h1:CdVq2IwM+JU9D6TnWiQSg9lqZWu6zUfK67YXET2LqIM=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
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/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/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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
|
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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
|
golang.org/x/sync v0.5.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.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
|
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||||
|
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||||
|
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
546
main.go
Normal file
546
main.go
Normal file
@ -0,0 +1,546 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
e "git.kjao.me/kjao/anitoru/err"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/gookit/config/v2"
|
||||||
|
"github.com/gookit/config/v2/yamlv3"
|
||||||
|
"github.com/mmcdole/gofeed"
|
||||||
|
"github.com/mrobinsn/go-rtorrent/rtorrent"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ReleaseURL string = "https://www.erai-raws.info/release-schedule/"
|
||||||
|
const RSSURL string = "https://www.erai-raws.info/feed/?res=1080p&type=magnet&subs%5B0%5D=us&v0=no&d157edc6b50f28b2776442c03d067d56"
|
||||||
|
const NyaaURL string = "https://nyaa.si/"
|
||||||
|
|
||||||
|
const SocketPath string = "/tmp/anitoru.sock"
|
||||||
|
const SubscriptionsFile string = "subscriptions.txt"
|
||||||
|
|
||||||
|
var InvalidDotfile error = errors.New("Dotfile invalid.")
|
||||||
|
|
||||||
|
type Configuration struct {
|
||||||
|
BaseDir string `config:"baseDir"`
|
||||||
|
RTorrentHost string `config:"rTorrentHost"`
|
||||||
|
PollingRate int `config:"pollingRate"`
|
||||||
|
DownloadDir string `config:"downloadDir"`
|
||||||
|
LogPath string `config:"logPath"`
|
||||||
|
NotifyError bool `config:"notifyError"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Anime struct {
|
||||||
|
Name string
|
||||||
|
Episode int
|
||||||
|
Magnet string
|
||||||
|
Version int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnime(link string) Anime {
|
||||||
|
return Anime{Magnet: link}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NTFYMessage(msg string) {
|
||||||
|
http.Post("https://ntfy.kjao.me/anitoru", "text/plain", strings.NewReader(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogNTFY() e.LogOption {
|
||||||
|
return func(msg string) {
|
||||||
|
NTFYMessage(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogStd() e.LogOption {
|
||||||
|
return func(msg string) {
|
||||||
|
fmt.Println(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSubMap(subscriptions *[]string) map[string]struct{} {
|
||||||
|
subMap := make(map[string]struct{})
|
||||||
|
for _, sub := range *subscriptions {
|
||||||
|
var d struct{}
|
||||||
|
subMap[sub] = d
|
||||||
|
}
|
||||||
|
return subMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSubList(subMap *map[string]struct{}) []string {
|
||||||
|
subscriptions := make([]string, len(*subMap), len(*subMap))
|
||||||
|
i := 0
|
||||||
|
for sub, _ := range *subMap {
|
||||||
|
subscriptions[i] = sub
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return subscriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ani *Anime) InfoFromTitle(title string) error {
|
||||||
|
titleSplit := strings.Split(title, " ")
|
||||||
|
ani.Name = strings.Join(titleSplit[:len(titleSplit)-2], " ")
|
||||||
|
|
||||||
|
epInfo := strings.Split(titleSplit[len(titleSplit)-1], "v")
|
||||||
|
ep, err := strconv.Atoi(epInfo[0])
|
||||||
|
return e.If(err).ThenIf(func() error {
|
||||||
|
ani.Episode = ep
|
||||||
|
if len(epInfo) > 1 {
|
||||||
|
ani.Version, err = strconv.Atoi(epInfo[1])
|
||||||
|
return e.If(err).End()
|
||||||
|
} else {
|
||||||
|
ani.Version = 1
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}).End()
|
||||||
|
}
|
||||||
|
|
||||||
|
// func Scheduler(clock string) <-chan int {
|
||||||
|
// c := make(chan int)
|
||||||
|
// clockTime, err := time.Parse("05:15", clock)
|
||||||
|
// FatalErr(err)
|
||||||
|
|
||||||
|
// year, month, day := time.Now().Date()
|
||||||
|
// scheduleDate := time.Date(year, month, day, clockTime.Hour(), clockTime.Minute(),
|
||||||
|
// 0, 0, time.UTC).AddDate(0, 0, 1)
|
||||||
|
|
||||||
|
// go func(date time.Time) {
|
||||||
|
// for {
|
||||||
|
// if time.Now().After(date) {
|
||||||
|
// c <- 1
|
||||||
|
// date = date.AddDate(0, 0, 1)
|
||||||
|
// } else {
|
||||||
|
// time.Sleep(time.Minute)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }(scheduleDate)
|
||||||
|
// return c
|
||||||
|
// }
|
||||||
|
|
||||||
|
func GetReleaseSchedule() ([]string, error) {
|
||||||
|
var doc *goquery.Document
|
||||||
|
var titles []string
|
||||||
|
|
||||||
|
res, err := http.Get(ReleaseURL)
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
return titles, e.If(err).ThenIf(func() error { // The function will run first before return.
|
||||||
|
doc, err = goquery.NewDocumentFromReader(res.Body)
|
||||||
|
return err
|
||||||
|
}).Then(func() {
|
||||||
|
doc.Find("table .aa_ss_ops_new").Each(func(i int, s *goquery.Selection) {
|
||||||
|
titles = append(titles, strings.TrimSpace(s.Text()))
|
||||||
|
})
|
||||||
|
}).End()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRSS() ([]Anime, error) {
|
||||||
|
var animes []Anime
|
||||||
|
|
||||||
|
fp := gofeed.NewParser()
|
||||||
|
feed, err := fp.ParseURL(RSSURL)
|
||||||
|
|
||||||
|
return animes, e.If(err).Then(func() {
|
||||||
|
for _, item := range feed.Items {
|
||||||
|
title := strings.Split(item.Title, "[Magnet] ")[1]
|
||||||
|
title = strings.Split(title, " [1080p]")[0]
|
||||||
|
if title[len(title)-6:] != "(HEVC)" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
title = title[:len(title)-7]
|
||||||
|
anime := NewAnime(item.Link)
|
||||||
|
e.LogIf(anime.InfoFromTitle(title))
|
||||||
|
animes = append(animes, anime)
|
||||||
|
}
|
||||||
|
}).End()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllAvailable(name string) ([]Anime, error) {
|
||||||
|
var animes []Anime
|
||||||
|
var res *http.Response
|
||||||
|
var doc *goquery.Document
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", NyaaURL, nil)
|
||||||
|
|
||||||
|
return animes, e.If(err).ThenIf(func() error {
|
||||||
|
q := req.URL.Query()
|
||||||
|
query := "[Erai-raws] " + name + " [HEVC]"
|
||||||
|
query = strings.Replace(query, ":", " ", -1)
|
||||||
|
query = strings.Replace(query, ".", " ", -1)
|
||||||
|
q.Add("q", query)
|
||||||
|
q.Add("s", "id") // Sort by date and descending
|
||||||
|
q.Add("o", "desc")
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
res, err = (&http.Client{}).Do(req)
|
||||||
|
return err
|
||||||
|
}).ThenIf(func() error {
|
||||||
|
defer res.Body.Close()
|
||||||
|
doc, err = goquery.NewDocumentFromReader(res.Body)
|
||||||
|
return err
|
||||||
|
}).Then(func() {
|
||||||
|
var epMap = make(map[int]struct{})
|
||||||
|
doc.Find(".success").Each(func(i int, s *goquery.Selection) {
|
||||||
|
cols := s.Children()
|
||||||
|
cols = cols.Next() // Filename
|
||||||
|
title := cols.Text()
|
||||||
|
|
||||||
|
if !strings.Contains(title, "[HEVC]") || !strings.Contains(title, "[ENG]") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
title = strings.Split(title, "[Erai-raws] ")[1]
|
||||||
|
if strings.Contains(title, "(AAC 2.0)") {
|
||||||
|
title = strings.Split(title, " (AAC 2.0)")[0]
|
||||||
|
} else {
|
||||||
|
title = strings.Split(title, " [1080p]")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
cols = cols.Next() // Download link
|
||||||
|
link := cols.Children().Next()
|
||||||
|
anime := NewAnime(link.AttrOr("href", "none"))
|
||||||
|
e.LogIf(anime.InfoFromTitle(title))
|
||||||
|
anime.Name = name // Actual name, since filenames remove special characters.
|
||||||
|
|
||||||
|
_, exists := epMap[anime.Episode]
|
||||||
|
if !exists {
|
||||||
|
animes = append(animes, anime)
|
||||||
|
var s struct{}
|
||||||
|
epMap[anime.Episode] = s
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).End()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SocketListener(sock net.Listener) chan net.Conn {
|
||||||
|
c := make(chan net.Conn)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := sock.Accept()
|
||||||
|
if e.LogIf(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c <- conn
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
type Daemon struct {
|
||||||
|
Tor *rtorrent.RTorrent
|
||||||
|
PollingRate time.Duration
|
||||||
|
DownloadDir string
|
||||||
|
Listener chan net.Conn
|
||||||
|
Subscriptions map[string]struct{}
|
||||||
|
LogOptions []e.LogOption
|
||||||
|
SubscriptionsPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDaemon(server string, polling int, download string, socket net.Listener,
|
||||||
|
notifyError bool, subPath string) Daemon {
|
||||||
|
// Connect to rTorrent
|
||||||
|
tor := rtorrent.New(server, false)
|
||||||
|
name, err := tor.Name()
|
||||||
|
|
||||||
|
e.ExitIf(err, "Unable to connect to rTorrent.")
|
||||||
|
fmt.Fprintln(os.Stdout, "Connected to", name, "at", server)
|
||||||
|
|
||||||
|
// Create simple socket for subscriptions
|
||||||
|
listener := SocketListener(socket)
|
||||||
|
|
||||||
|
// Make subscriptions if doesn't exist.
|
||||||
|
subFile, err := os.OpenFile(subPath, os.O_RDONLY|os.O_CREATE, 0644)
|
||||||
|
e.ExitIf(err, "Unable to open subscriptions file.")
|
||||||
|
subFile.Close()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(subPath)
|
||||||
|
e.ExitIf(err, "Unable to read subscriptions file.")
|
||||||
|
var subscriptions []string
|
||||||
|
if len(data) == 0 {
|
||||||
|
subscriptions = []string{}
|
||||||
|
} else {
|
||||||
|
subscriptions = strings.Split(string(data), "\n")
|
||||||
|
}
|
||||||
|
subMap := GetSubMap(&subscriptions)
|
||||||
|
|
||||||
|
var opt []e.LogOption
|
||||||
|
if notifyError {
|
||||||
|
opt = []e.LogOption{LogNTFY(), LogStd()}
|
||||||
|
} else {
|
||||||
|
opt = []e.LogOption{LogStd()}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Daemon{
|
||||||
|
Tor: tor,
|
||||||
|
PollingRate: time.Duration(polling),
|
||||||
|
DownloadDir: download,
|
||||||
|
Listener: listener,
|
||||||
|
Subscriptions: subMap,
|
||||||
|
LogOptions: opt,
|
||||||
|
SubscriptionsPath: subPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) Serve() {
|
||||||
|
//d.CheckRSS()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(d.PollingRate * time.Minute):
|
||||||
|
d.CheckRSS()
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
d.CheckTorrents()
|
||||||
|
case conn := <-d.Listener:
|
||||||
|
d.ReadSocket(conn)
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) CheckRSS() {
|
||||||
|
animes, err := GetRSS()
|
||||||
|
if e.LogIf(err, d.LogOptions...) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, anime := range animes {
|
||||||
|
_, exists := d.Subscriptions[anime.Name]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.LogIf(d.Download(&anime), d.LogOptions...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) CheckTorrents() {
|
||||||
|
torrents, err := d.Tor.GetTorrents(rtorrent.ViewMain)
|
||||||
|
e.LogIf(err, d.LogOptions...)
|
||||||
|
for _, torrent := range torrents {
|
||||||
|
if torrent.Completed {
|
||||||
|
d.Tor.Delete(torrent)
|
||||||
|
arr := strings.Split(torrent.Label, "\n")
|
||||||
|
name := arr[0]
|
||||||
|
dir := arr[1]
|
||||||
|
ext := filepath.Ext(torrent.Name)
|
||||||
|
err := os.Rename(torrent.Path+"/"+torrent.Name, dir+"/"+name+ext)
|
||||||
|
e.LogIf(err, d.LogOptions...)
|
||||||
|
e.Log(fmt.Sprintf("Finished downloading %v.", name), LogNTFY())
|
||||||
|
} else {
|
||||||
|
active, err := d.Tor.IsActive(torrent)
|
||||||
|
e.LogIf(err, d.LogOptions...)
|
||||||
|
fmt.Println(torrent.Name, "Active:", active)
|
||||||
|
d.Tor.ResumeTorrent(torrent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) SendSubs(conn net.Conn) {
|
||||||
|
subs := strings.Join(GetSubList(&d.Subscriptions), "\n")
|
||||||
|
err := SendData(conn, []byte(subs))
|
||||||
|
e.LogIf(err, d.LogOptions...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) ReadSocket(conn net.Conn) {
|
||||||
|
rawData, err := ReadData(conn)
|
||||||
|
if e.LogIf(err, d.LogOptions...) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
command := rawData[0]
|
||||||
|
data := string(rawData[1:len(rawData)])
|
||||||
|
e.Log("Socket Command: " + string(command))
|
||||||
|
newSubs := strings.Split(data, "\n")
|
||||||
|
|
||||||
|
var write bool
|
||||||
|
switch command {
|
||||||
|
case 'A':
|
||||||
|
// Add each subscription
|
||||||
|
for _, sub := range newSubs {
|
||||||
|
var s struct{}
|
||||||
|
d.Subscriptions[sub] = s
|
||||||
|
}
|
||||||
|
d.SendSubs(conn)
|
||||||
|
|
||||||
|
for _, sub := range newSubs {
|
||||||
|
// For each new subscription, get all available animes.
|
||||||
|
animes, err := GetAllAvailable(sub)
|
||||||
|
if e.LogIf(err, d.LogOptions...) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, anime := range animes {
|
||||||
|
e.LogIf(d.Download(&anime), d.LogOptions...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write = true
|
||||||
|
case 'R':
|
||||||
|
for _, sub := range strings.Split(data, "\n") {
|
||||||
|
delete(d.Subscriptions, sub)
|
||||||
|
}
|
||||||
|
write = true
|
||||||
|
d.SendSubs(conn)
|
||||||
|
case 'V':
|
||||||
|
write = false
|
||||||
|
d.SendSubs(conn)
|
||||||
|
default:
|
||||||
|
e.Log("Invalid Command")
|
||||||
|
}
|
||||||
|
|
||||||
|
if write { // Write subscriptions to file.
|
||||||
|
f, err := os.Create(d.SubscriptionsPath)
|
||||||
|
if e.LogIf(err, d.LogOptions...) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
f.Write([]byte(strings.Join(GetSubList(&d.Subscriptions), "\n")))
|
||||||
|
f.Sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) Download(ani *Anime) error {
|
||||||
|
aniDir := filepath.Join(d.DownloadDir, ani.Name)
|
||||||
|
dotPath := filepath.Join(aniDir, ".anitoru")
|
||||||
|
var dot map[int]int
|
||||||
|
|
||||||
|
err := os.MkdirAll(aniDir, os.ModePerm)
|
||||||
|
return e.If(err).ThenIf(func() error {
|
||||||
|
dot, err = LoadDotfile(dotPath)
|
||||||
|
return err
|
||||||
|
}).Then(func() {
|
||||||
|
vers, exists := dot[ani.Episode]
|
||||||
|
if exists || vers >= ani.Version { // No need to download.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := fmt.Sprintf("%v - %02d", ani.Name, ani.Episode)
|
||||||
|
d.Tor.Add(ani.Magnet,
|
||||||
|
rtorrent.DLabel.SetValue(name+"\n"+aniDir),
|
||||||
|
)
|
||||||
|
e.Log(fmt.Sprintf("Queueing download of %v...", name), LogNTFY())
|
||||||
|
dot[ani.Episode] = ani.Version
|
||||||
|
SaveDotfile(dotPath, dot)
|
||||||
|
}).End()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendData(conn net.Conn, data []byte) error {
|
||||||
|
length := make([]byte, 4)
|
||||||
|
binary.LittleEndian.PutUint32(length, uint32(len(data)))
|
||||||
|
_, err := conn.Write(append(length, data...))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadData(conn net.Conn) ([]byte, error) {
|
||||||
|
var data []byte
|
||||||
|
lengthBuf := make([]byte, 4)
|
||||||
|
_, err := conn.Read(lengthBuf)
|
||||||
|
return data, e.If(err).Then(func() {
|
||||||
|
length := binary.LittleEndian.Uint32(lengthBuf)
|
||||||
|
data = make([]byte, length)
|
||||||
|
}).ThenIf(func() error {
|
||||||
|
_, err = conn.Read(data)
|
||||||
|
return err
|
||||||
|
}).End()
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadDotfile(path string) (map[int]int, error) {
|
||||||
|
dot := make(map[int]int)
|
||||||
|
|
||||||
|
// Check if exists.
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return dot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
return dot, e.If(err).ThenIf(func() error {
|
||||||
|
eps := strings.Split(string(data), "\n")
|
||||||
|
for _, epInfo := range eps {
|
||||||
|
arr := strings.Split(epInfo, " ")
|
||||||
|
if len(arr) != 2 {
|
||||||
|
return InvalidDotfile
|
||||||
|
}
|
||||||
|
ep, err1 := strconv.Atoi(arr[0])
|
||||||
|
v, err2 := strconv.Atoi(arr[1])
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
return InvalidDotfile
|
||||||
|
}
|
||||||
|
dot[ep] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}).End()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveDotfile(path string, dot map[int]int) error {
|
||||||
|
var epInfo = make([]string, len(dot), len(dot))
|
||||||
|
i := 0
|
||||||
|
for ep, v := range dot {
|
||||||
|
epInfo[i] = fmt.Sprintf("%v %v", ep, v)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, []byte(strings.Join(epInfo, "\n")), 0666)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig(configPath string) Configuration {
|
||||||
|
config.WithOptions(func(options *config.Options) {
|
||||||
|
options.DecoderConfig.TagName = "config"
|
||||||
|
options.ParseEnv = true
|
||||||
|
options.Readonly = true
|
||||||
|
})
|
||||||
|
config.AddDriver(yamlv3.Driver)
|
||||||
|
var conf Configuration
|
||||||
|
e.ExitIf(config.LoadFiles(configPath), "Unable to load config.")
|
||||||
|
config.Decode(&conf)
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
daemonize := flag.Bool("daemon", false, "Run the daemon")
|
||||||
|
config := flag.String("config", "server.yml", "Config path")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *daemonize {
|
||||||
|
conf := loadConfig(*config)
|
||||||
|
|
||||||
|
f, err := os.OpenFile(conf.LogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0744)
|
||||||
|
e.ExitIf(err, "Unable to open log file.")
|
||||||
|
defer f.Close()
|
||||||
|
log.SetOutput(f)
|
||||||
|
|
||||||
|
os.Remove(SocketPath)
|
||||||
|
sock, err := net.Listen("unix", SocketPath)
|
||||||
|
e.ExitIf(err, "Unable to open socket.")
|
||||||
|
defer sock.Close()
|
||||||
|
err = os.Chmod(SocketPath, 0777)
|
||||||
|
|
||||||
|
// For cleanup, in case of signal.
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
|
go func() {
|
||||||
|
for _ = range c {
|
||||||
|
f.Close()
|
||||||
|
sock.Close()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Run daemon
|
||||||
|
daemon := NewDaemon(
|
||||||
|
conf.RTorrentHost, conf.PollingRate, conf.DownloadDir, sock,
|
||||||
|
conf.NotifyError, filepath.Join(conf.BaseDir, SubscriptionsFile),
|
||||||
|
)
|
||||||
|
daemon.Serve()
|
||||||
|
} else {
|
||||||
|
userCLI()
|
||||||
|
}
|
||||||
|
}
|
||||||
6
server.yml
Normal file
6
server.yml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
baseDir: ./
|
||||||
|
rTorrentHost: http://localhost/rTorrent
|
||||||
|
pollingRate: 15
|
||||||
|
downloadDir: /data/Videos/Anime/
|
||||||
|
logPath: log.log
|
||||||
|
notifyError: true
|
||||||
Loading…
x
Reference in New Issue
Block a user