194 lines
4.6 KiB
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))
|
|
}
|
|
}
|