1
0
Fork 0
videodog/videodog.go

194 lines
4.6 KiB
Go

// SPDX-License-Identifier: GPL-3.0-or-later
package main
import (
"errors"
"flag"
"fmt"
"log"
"os"
"os/signal"
"github.com/go-co-op/gocron/v2"
)
var (
config *Config // The execution context of the application.
spool *Spool // The spool used to store the system data.
flagConfig string // The config file that should be used.
flagNoannounce bool // Whether to avoid announcing the videos (part of dry-run mode)
flagCheckOnStart bool // Make a quick check when the application starts
)
// The different errors used by the application.
var (
ErrNoConfigFile = errors.New("missing config file")
ErrInvalidConfigFile = errors.New("invalid config file")
ErrSpoolInitialization = errors.New("error initializing spool")
ErrSpoolOperation = errors.New("error operating spool")
ErrSchedulerConfig = errors.New("cannot configure scheduler")
ErrFeedFetcher = errors.New("feed fetching error")
ErrAnnouncer = errors.New("announce hook error")
)
func main() {
// Startup application.
parseArgs()
initContext()
defer spool.Close()
// If the configuration says that we must autodiscard entries on start, do it now.
if config.Autospool {
autospoolFeeds()
}
// Make a first inspection.
if flagCheckOnStart {
for key, channel := range config.Channels {
checkChannel(key, channel)
}
}
// Exec daemon until we receive SIGINT.
s := startDaemon()
// Wait for SIGINT.
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, os.Interrupt)
select {
case <-sigch:
}
if err := s.Shutdown(); err != nil {
fmt.Println(err)
handleError(err)
os.Exit(1)
}
}
func initContext() {
file, err := os.Open(flagConfig)
if err != nil {
handleError(errors.Join(ErrInvalidConfigFile, err))
os.Exit(1)
}
defer file.Close()
config, err = ReadConfigObject(file)
if err != nil {
handleError(errors.Join(ErrInvalidConfigFile, err))
os.Exit(1)
}
spool, err = NewSpool(config.Spoolfile)
if err != nil {
handleError(errors.Join(ErrSpoolInitialization, err))
os.Exit(1)
}
}
func parseArgs() {
flag.StringVar(&flagConfig, "config", "", "The config file to use")
flag.BoolVar(&flagNoannounce, "noannounce", false, "Don't announce the videos")
flag.BoolVar(&flagCheckOnStart, "checkstart", false, "Check when the server starts (do not wait until next cron interval)")
flag.Parse()
if flagConfig == "" {
flag.PrintDefaults()
os.Exit(0)
}
}
// Fetches each feed and marks all the content in the feeds as announced.
// Prevents a ping-storm on first boot of the application. Only videos uploaded
// since the daemon starts are announced.
func autospoolFeeds() {
for key, channel := range config.Channels {
videos := fetch(channel)
for _, video := range videos {
if !announced(key, &video) {
mark(key, &video)
}
}
}
}
// Starts the scheduler and lets it block the application.
func startDaemon() gocron.Scheduler {
/* This is the daemon where the tasks will be scheduled. */
s, err := gocron.NewScheduler()
if err != nil {
handleError(errors.Join(ErrSchedulerConfig, err))
os.Exit(1)
}
j, err := s.NewJob(
gocron.CronJob("0 * * * *", false),
gocron.NewTask(func() {
for key, channel := range config.Channels {
checkChannel(key, channel)
}
}),
)
if err != nil {
handleError(errors.Join(ErrSchedulerConfig, err))
os.Exit(1)
}
log.Printf("Feed checker task scheduled with ID %s\n", j.ID())
s.Start()
return s
}
func checkChannel(key string, channel *Channel) {
videos := fetch(channel)
for _, video := range videos {
if !announced(key, &video) {
if flagNoannounce {
log.Printf("Skipping announce because running in dry-mode")
} else {
announce(channel, &video)
}
mark(key, &video)
// only one video per iteration, prevents ping-storms
return
}
}
}
func handleError(err error) {
/* TODO: Send this to a different error queue? Meta-webhooks? */
log.Printf("Error: %s", err.Error())
}
func fetch(channel *Channel) []Video {
videos, err := FetchYouTubeFeed(channel.Feed)
if err != nil {
handleError(errors.Join(ErrFeedFetcher, err))
return []Video{}
}
return videos
}
func announced(channel string, video *Video) bool {
done, err := spool.IsAnnounced(channel, video.VideoId)
if err != nil {
handleError(errors.Join(ErrSpoolOperation, err))
}
return done
}
func announce(channel *Channel, video *Video) {
if err := AnnounceVideo(channel.Webhook, channel.Role, video); err != nil {
handleError(errors.Join(ErrAnnouncer, err))
}
}
func mark(channel string, video *Video) {
if err := spool.MarkAsAnnounced(channel, video.VideoId); err != nil {
handleError(errors.Join(ErrSpoolOperation, err))
}
}