219 lines
4.9 KiB
Go
219 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
"git.meatbag.se/varl/subsyt/internal/config"
|
|
"git.meatbag.se/varl/subsyt/internal/dl"
|
|
"git.meatbag.se/varl/subsyt/internal/format"
|
|
"git.meatbag.se/varl/subsyt/internal/fsops"
|
|
"git.meatbag.se/varl/subsyt/internal/metadata"
|
|
"git.meatbag.se/varl/subsyt/internal/organize"
|
|
"git.meatbag.se/varl/subsyt/internal/scheduler"
|
|
"git.meatbag.se/varl/subsyt/internal/server"
|
|
)
|
|
|
|
func run(cfg config.Config) {
|
|
paths := cfg.Paths()
|
|
organizer := organize.Organizer{
|
|
Paths: paths,
|
|
FS: fsops.Manager{DryRun: cfg.Dry_run},
|
|
}
|
|
|
|
if err := organizer.Prepare(); err != nil {
|
|
log.Fatalf("prepare directories: %v", err)
|
|
}
|
|
|
|
provider := cfg.Provider["youtube"]
|
|
|
|
if err := dl.UpgradeYtDlp(provider.Cmd); err != nil {
|
|
log.Fatalf("failed to ensure yt-dlp is up to date: %v", err)
|
|
}
|
|
|
|
opml, err := format.OpmlLoad(provider.Opml_file)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
for _, outlines := range opml.Body.Outline {
|
|
log.Printf("Archiving videos from OPML: %s\n", outlines.Title)
|
|
|
|
for _, outline := range outlines.Outlines {
|
|
rssData, err := dl.RssDownloader(outline.XmlUrl)
|
|
if err != nil {
|
|
log.Printf("Failed to download RSS for %s: %v", outline.Title, err)
|
|
continue
|
|
}
|
|
|
|
feed, err := format.RssLoad(rssData)
|
|
if err != nil {
|
|
log.Printf("Failed to parse RSS for %s: %v", feed.Title, err)
|
|
continue
|
|
}
|
|
|
|
// download the channel
|
|
dl.Youtube(dl.Download{
|
|
Url: feed.Author.Uri,
|
|
OutDir: paths.ChannelsDir,
|
|
DryRun: cfg.Dry_run,
|
|
Metadata: true,
|
|
}, provider)
|
|
|
|
log.Printf("Downloaded RSS feed for %s with %d entries", feed.Title, len(feed.Entries))
|
|
|
|
for _, entry := range feed.Entries {
|
|
url := fmt.Sprintf("%s/watch?v=%s", provider.Url, entry.VideoId)
|
|
|
|
log.Printf("Entry: %#v", entry)
|
|
dl.Youtube(dl.Download{
|
|
Url: url,
|
|
OutDir: paths.EpisodesDir,
|
|
DryRun: cfg.Dry_run,
|
|
Metadata: false,
|
|
}, provider)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := syncLibrary(organizer); err != nil {
|
|
log.Fatalf("organize library: %v", err)
|
|
}
|
|
}
|
|
|
|
func syncLibrary(organizer organize.Organizer) error {
|
|
shows, err := metadata.ScanShows(organizer.Paths.ChannelsDir)
|
|
if err != nil {
|
|
return fmt.Errorf("scan shows: %w", err)
|
|
}
|
|
|
|
if err := organizer.ProcessShows(shows); err != nil {
|
|
return fmt.Errorf("process shows: %w", err)
|
|
}
|
|
|
|
episodes, err := metadata.ScanEpisodes(organizer.Paths.EpisodesDir)
|
|
if err != nil {
|
|
return fmt.Errorf("scan episodes: %w", err)
|
|
}
|
|
|
|
if err := organizer.ProcessEpisodes(episodes); err != nil {
|
|
return fmt.Errorf("process episodes: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
configPtr := flag.String("config", "", "path to config file")
|
|
flag.Parse()
|
|
|
|
configEnv := os.Getenv("CONFIG")
|
|
|
|
var configPath string
|
|
if *configPtr != "" {
|
|
configPath = *configPtr
|
|
} else if configEnv != "" {
|
|
configPath = configEnv
|
|
} else {
|
|
configPath = "./config.json"
|
|
}
|
|
|
|
log.Println("resolved config file", configPath)
|
|
|
|
cfg, err := config.Load(configPath)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
provider, ok := cfg.Provider["youtube"]
|
|
if !ok {
|
|
log.Fatal("youtube provider configuration missing")
|
|
}
|
|
|
|
if cfg.Http_api.Enable {
|
|
if err := setupAPIServer(ctx, cfg, provider); err != nil {
|
|
log.Fatalf("failed to start http api: %v", err)
|
|
}
|
|
}
|
|
|
|
if cfg.Daemon {
|
|
log.Println("running with scheduler")
|
|
s := scheduler.Scheduler{}
|
|
s.Start(run, cfg)
|
|
} else {
|
|
log.Println("running standalone")
|
|
run(cfg)
|
|
}
|
|
}
|
|
|
|
func setupAPIServer(ctx context.Context, cfg config.Config, provider config.Provider) error {
|
|
paths := cfg.Paths()
|
|
|
|
queue, err := server.NewQueue(cfg.Http_api.Queue_file)
|
|
if err != nil {
|
|
return fmt.Errorf("load queue: %w", err)
|
|
}
|
|
|
|
go queueWorker(ctx, queue, cfg, provider)
|
|
|
|
srv := server.NewServer(cfg.Http_api, paths.MediaDir, queue)
|
|
go func() {
|
|
if err := srv.Start(ctx); err != nil {
|
|
log.Printf("http api server stopped with error: %v", err)
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func queueWorker(ctx context.Context, q *server.Queue, cfg config.Config, provider config.Provider) {
|
|
organizer := organize.Organizer{
|
|
Paths: cfg.Paths(),
|
|
FS: fsops.Manager{DryRun: cfg.Dry_run},
|
|
}
|
|
|
|
if err := organizer.Prepare(); err != nil {
|
|
log.Printf("queue prepare error: %v", err)
|
|
return
|
|
}
|
|
|
|
for {
|
|
item, err := q.Next(ctx)
|
|
if err != nil {
|
|
if errors.Is(err, context.Canceled) {
|
|
return
|
|
}
|
|
log.Printf("queue wait error: %v", err)
|
|
select {
|
|
case <-time.After(5 * time.Second):
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
continue
|
|
}
|
|
|
|
dl.Youtube(dl.Download{
|
|
Url: item.Request.URL,
|
|
OutDir: organizer.Paths.EpisodesDir,
|
|
DryRun: cfg.Dry_run,
|
|
Metadata: false,
|
|
}, provider)
|
|
|
|
if err := syncLibrary(organizer); err != nil {
|
|
log.Printf("queue organize error: %v", err)
|
|
}
|
|
|
|
if err := q.MarkDone(item.ID); err != nil {
|
|
log.Printf("queue mark done failed: %v", err)
|
|
}
|
|
}
|
|
}
|