-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdaemon.go
More file actions
211 lines (183 loc) · 5.77 KB
/
daemon.go
File metadata and controls
211 lines (183 loc) · 5.77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Daemon loop that runs in the background
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"videoarchiver/backend/domains/download"
"videoarchiver/backend/domains/ytdlp"
)
var (
app *App
cancelFunc context.CancelFunc
lastRun time.Time = time.Time{}
)
const (
daemonWorkCheckInterval = 5 * time.Second
daemonPlaylistCheckInterval = 30 * time.Minute
)
func startDaemonLoop(_app *App) {
app = _app
app.LogService.Info("Starting daemon loop...")
// Create context and shutdown handling here
ctx, _cancelFunc := context.WithCancel(context.Background())
cancelFunc = _cancelFunc
// Handle shutdown signals
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
app.LogService.Info("Shutdown signal received")
cancelFunc()
}()
for {
select {
case <-ctx.Done():
app.LogService.Info("Daemon loop shutting down")
return
default:
// Check if we need to do work due to time elapsed
doWork := lastRun.Add(daemonPlaylistCheckInterval).Before(time.Now())
if doWork {
app.LogService.Info("Running iteration: time elapsed since last run")
}
// Check if we need to do work due to change signal
if !doWork {
isChangeTriggered, err := app.DaemonSignalService.IsChangeTriggered()
if err != nil {
app.LogService.Error(fmt.Sprintf("Failed to check if change is triggered: %v", err))
cancelFunc()
return
}
if isChangeTriggered {
app.LogService.Info("Running iteration: change triggered by UI")
err := app.DaemonSignalService.ClearChangeTrigger()
if err != nil {
app.LogService.Error(fmt.Sprintf("Failed to clear change trigger: %v", err))
cancelFunc()
return
}
doWork = true
}
}
// Do work if needed
if doWork {
lastRun = time.Now()
processActivePlaylists()
}
// Then wait 5s (or until cancelled)
select {
case <-ctx.Done():
// Break out of the inner select triggering the outer one
return
case <-time.After(daemonWorkCheckInterval):
// Continue to next iteration
continue
}
}
}
}
// Process playlists
func processActivePlaylists() {
app.LogService.Info("Processing playlists...")
// Ensure credentials are cleaned up after processing
// (credentials may be created during retry attempts for private/age-restricted videos)
defer func() {
if err := ytdlp.CleanupCredentialsFile(app.LogService); err != nil {
app.LogService.Warn(fmt.Sprintf("Failed to cleanup credentials file: %v", err))
}
}()
// Get acive playlists
activePlaylists, err := app.PlaylistDB.GetActivePlaylists()
if err != nil {
app.LogService.Error(fmt.Sprintf("Failed to get active playlists: %v", err))
return
}
// Loop over active playlists
for _, pl := range activePlaylists {
app.LogService.Info(fmt.Sprintf("Processing playlist: %s", pl.Name))
// Get playlist items online
plInfo, err := ytdlp.GetPlaylistInfoFlat(pl.URL)
if err != nil {
app.LogService.Error(fmt.Sprintf("Failed to get playlist info for %s: %v", pl.Name, err))
continue
}
// Check which playlist items are already processed
existingDls, err := app.DownloadDB.GetDownloadsForPlaylist(pl.ID)
if err != nil {
app.LogService.Error(fmt.Sprintf("Failed to get existing downloads for playlist %s: %v", pl.Name, err))
continue
}
// Filter out already downloaded urls
retryables, undownloadedUrls := getDownloadables(plInfo, existingDls)
if len(undownloadedUrls) == 0 && len(retryables) == 0 {
app.LogService.Debug(fmt.Sprintf("No new items or retryable to download for playlist: %s", pl.Name))
continue
}
app.LogService.Info(fmt.Sprintf("Found %d new items and %d retryable items to download for playlist: %s",
len(undownloadedUrls), len(retryables), pl.Name))
// Retry any retryable items
for _, dl := range retryables {
if shouldStopIteration() {
return
}
app.DownloadService.ArchiveDownloadFile(&dl, &pl)
}
// Download any new items
for _, url := range undownloadedUrls {
if shouldStopIteration() {
return
}
dl := download.NewDownload(pl.ID, url, pl.OutputFormat)
app.DownloadService.ArchiveDownloadFile(dl, &pl)
}
}
app.LogService.Info("Playlist processing complete.")
}
// Get undownloaded and retryable items from playlist info and existing downloads
func getDownloadables(plInfo *ytdlp.YtdlpPlaylistInfo, existingDls []download.Download) ([]download.Download, []string) {
// Prepare return values
retryables := make([]download.Download, 0)
undownloadedUrls := make([]string, 0)
// Create map of existing entries for quick lookup
existingMap := make(map[string]bool)
for _, existintEntry := range existingDls {
// Add every existing item to the existing map
existingMap[existintEntry.Url] = true
// Add redownloadable items to result
if existintEntry.Status == download.StFailedAutoRetry || existintEntry.Status == download.StFailedManualRetry {
retryables = append(retryables, existintEntry)
}
}
// Create download entries for new items
for _, item := range plInfo.Entries {
if _, exists := existingMap[item.URL]; !exists {
undownloadedUrls = append(undownloadedUrls, item.URL)
}
}
// Return results
return retryables, undownloadedUrls
}
func shouldStopIteration() bool {
// Check for shutdown signal
select {
case <-context.Background().Done():
app.LogService.Info("Shutdown signal received, stopping downloads")
return true
default:
// Check for daemon change signal
isChangeTriggered, err := app.DaemonSignalService.IsChangeTriggered()
if err != nil {
app.LogService.Error(fmt.Sprintf("Failed to check if change is triggered: %v", err))
return true
}
if isChangeTriggered {
app.LogService.Info("Change triggered by UI, stopping downloads to restart iteration")
return true
}
}
return false
}