Initial commit

This commit is contained in:
Kenneth Jao 2024-08-05 18:48:28 -04:00
commit f1191196d5
8 changed files with 973 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.log
*.txt
anitoru

14
anitoru.service Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
baseDir: ./
rTorrentHost: http://localhost/rTorrent
pollingRate: 15
downloadDir: /data/Videos/Anime/
logPath: log.log
notifyError: true