-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.go
More file actions
381 lines (355 loc) · 11.2 KB
/
app.go
File metadata and controls
381 lines (355 loc) · 11.2 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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
package main
import (
"context"
"os"
"os/exec"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
// App is the main application struct, bound to the Wails frontend.
// It holds per-platform authentication state, the active download directory,
// and cancellation controls shared across goroutines.
type App struct {
ctx context.Context // Wails runtime context, set on startup
activePlatform string // currently selected platform ("kiwify", "gumroad", etc.)
downloadDir string // root folder where courses are saved
cancel atomic.Bool // signals running downloads to stop
ytdlpCmd *exec.Cmd // current yt-dlp process, if any (for kill on cancel)
mu sync.Mutex // protects ytdlpCmd
kiwify KiwifyState
gumroad GumroadState
hotmart HotmartState
teachable TeachableState
kajabi KajabiState
skool SkoolState
pluralsight PluralSightState
greatcourses GreatCoursesState
masterclass MasterClassState
thinkific ThinkificState
}
// NewApp creates a fresh App with zero-value state (no platform selected).
func NewApp() *App {
return &App{}
}
// startup is called by Wails when the application window is ready.
// It stores the runtime context, injects the bundled-tools bin dir into PATH,
// and sets the default download directory.
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
injectBinDir() // make auto-downloaded yt-dlp/ffmpeg visible to exec.LookPath
a.downloadDir = defaultDownloadDir()
}
// SelectPlatform sets the active platform and returns the current config
// to the frontend (platform id + download folder path).
func (a *App) SelectPlatform(id string) map[string]interface{} {
a.activePlatform = id
return map[string]interface{}{
"platform": id,
"folder": a.downloadDir,
}
}
// GoBack cancels any running download and clears the active platform,
// returning the user to the platform selection screen.
func (a *App) GoBack() {
a.Cancel()
a.activePlatform = ""
}
// Login dispatches authentication to the active platform's login handler.
// Returns a map with "success" (bool) and optionally "error" (string)
// or "otp_sent" (bool) for platforms that use OTP (Teachable, Kajabi).
func (a *App) Login(email, password string) map[string]interface{} {
switch a.activePlatform {
case "kiwify":
return a.kiwifyLogin(email, password)
case "gumroad":
return a.gumroadLogin(email, password)
case "hotmart":
return a.hotmartLogin(email, password)
case "teachable":
return a.teachableLogin(email, password)
case "kajabi":
return a.kajabiLogin(email, password)
case "skool":
return a.skoolLogin(email, password)
case "pluralsight":
return a.pluralsightLogin(email, password)
case "greatcourses":
return a.greatcoursesLogin(email, password)
case "masterclass":
return a.masterclassLogin(email, password)
case "thinkific":
return a.thinkificLogin(email, password)
}
return map[string]interface{}{"success": false, "error": "Unknown platform"}
}
// GetCourses fetches the list of purchased courses for the active platform.
// Each entry contains name, preview image, and download status.
func (a *App) GetCourses() []map[string]interface{} {
switch a.activePlatform {
case "kiwify":
return a.kiwifyGetCourses()
case "gumroad":
return a.gumroadGetCourses()
case "hotmart":
return a.hotmartGetCourses()
case "teachable":
return a.teachableGetCourses()
case "kajabi":
return a.kajabiGetCourses()
case "skool":
return a.skoolGetCourses()
case "pluralsight":
return a.pluralsightGetCourses()
case "greatcourses":
return a.greatcoursesGetCourses()
case "masterclass":
return a.masterclassGetCourses()
case "thinkific":
return a.thinkificGetCourses()
}
return nil
}
// Download starts downloading the courses at the given indices in a background
// goroutine. The cancel flag is reset so previous cancellations don't interfere.
// Progress is reported to the frontend via Wails events.
func (a *App) Download(indices []int) {
a.cancel.Store(false)
switch a.activePlatform {
case "kiwify":
go a.kiwifyDownloadBatch(indices)
case "gumroad":
go a.gumroadDownloadBatch(indices)
case "hotmart":
go a.hotmartDownloadBatch(indices)
case "teachable":
go a.teachableDownloadBatch(indices)
case "kajabi":
go a.kajabiDownloadBatch(indices)
case "skool":
go a.skoolDownloadBatch(indices)
case "pluralsight":
go a.pluralsightDownloadBatch(indices)
case "greatcourses":
go a.greatcoursesDownloadBatch(indices)
case "masterclass":
go a.masterclassDownloadBatch(indices)
case "thinkific":
go a.thinkificDownloadBatch(indices)
}
}
// Cancel signals all running downloads to stop and kills any active yt-dlp process.
func (a *App) Cancel() {
a.cancel.Store(true)
a.mu.Lock()
if a.ytdlpCmd != nil && a.ytdlpCmd.Process != nil {
a.ytdlpCmd.Process.Kill()
}
a.mu.Unlock()
}
// PickFolder opens a native directory picker dialog and updates the download
// directory. Returns the selected (or unchanged) path.
func (a *App) PickFolder() string {
dir, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Select download folder",
DefaultDirectory: a.downloadDir,
})
if err != nil || dir == "" {
return a.downloadDir
}
a.downloadDir = dir
return dir
}
// courseFolderName returns the filesystem folder name for the course at index,
// or "" if the index is out of bounds or the platform is unknown.
func (a *App) courseFolderName(index int) string {
switch a.activePlatform {
case "kiwify":
if index >= 0 && index < len(a.kiwify.courses) {
return a.kiwify.courses[index].FolderName
}
case "gumroad":
if index >= 0 && index < len(a.gumroad.courses) {
return a.gumroad.courses[index].FolderName
}
case "hotmart":
if index >= 0 && index < len(a.hotmart.courses) {
return a.hotmart.courses[index].FolderName
}
case "teachable":
if index >= 0 && index < len(a.teachable.courses) {
return a.teachable.courses[index].FolderName
}
case "kajabi":
if index >= 0 && index < len(a.kajabi.courses) {
return a.kajabi.courses[index].FolderName
}
case "skool":
if index >= 0 && index < len(a.skool.groups) {
return a.skool.groups[index].FolderName
}
case "pluralsight":
if index >= 0 && index < len(a.pluralsight.courses) {
return a.pluralsight.courses[index].FolderName
}
case "greatcourses":
if index >= 0 && index < len(a.greatcourses.courses) {
return a.greatcourses.courses[index].FolderName
}
case "masterclass":
if index >= 0 && index < len(a.masterclass.courses) {
return a.masterclass.courses[index].FolderName
}
case "thinkific":
if index >= 0 && index < len(a.thinkific.courses) {
return a.thinkific.courses[index].FolderName
}
}
return ""
}
// DeleteCourse removes the entire download folder for the course at index.
func (a *App) DeleteCourse(index int) map[string]interface{} {
folder := a.courseFolderName(index)
if folder == "" {
return map[string]interface{}{"success": false, "error": "Invalid index"}
}
path := filepath.Join(a.platformDir(), folder)
if err := os.RemoveAll(path); err != nil {
return map[string]interface{}{"success": false, "error": err.Error()}
}
return map[string]interface{}{"success": true}
}
// OpenFolder opens the given path in the OS file explorer.
func (a *App) OpenFolder(path string) {
cmd := openFolderCmd(path)
cmd.Start()
}
// OpenCourseFolder opens the download folder for the course at index.
func (a *App) OpenCourseFolder(index int) {
if folder := a.courseFolderName(index); folder != "" {
a.OpenFolder(filepath.Join(a.platformDir(), folder))
}
}
// Logout cancels downloads and resets authentication state for the active platform.
func (a *App) Logout() {
a.Cancel()
switch a.activePlatform {
case "kiwify":
a.kiwify = KiwifyState{}
case "gumroad":
a.gumroad = GumroadState{}
case "hotmart":
a.hotmart = HotmartState{}
case "teachable":
a.teachable = TeachableState{}
case "kajabi":
a.kajabi = KajabiState{}
case "skool":
a.skool = SkoolState{}
case "pluralsight":
a.pluralsight = PluralSightState{}
case "greatcourses":
a.greatcourses = GreatCoursesState{}
case "masterclass":
a.masterclass = MasterClassState{}
case "thinkific":
a.thinkific = ThinkificState{}
}
}
// GetDownloadDir returns the current download directory path.
func (a *App) GetDownloadDir() string {
return a.downloadDir
}
// CheckDeps reports whether yt-dlp and ffmpeg are available.
// Checks the bundled bin dir first (stat), then falls back to PATH lookup.
// Called by the frontend on startup to decide whether to show the setup screen.
func (a *App) CheckDeps() map[string]bool {
return map[string]bool{
"ytdlp": depsHas("yt-dlp", ytdlpBin()),
"ffmpeg": depsHas("ffmpeg", ffmpegBin()),
}
}
// depsHas returns true if the tool exists in depsBinDir() OR in the system PATH.
func depsHas(name, binFile string) bool {
if _, err := os.Stat(filepath.Join(depsBinDir(), binFile)); err == nil {
return true
}
_, err := exec.LookPath(name)
return err == nil
}
// SelectSubgroup selects a school (Teachable) or site (Kajabi) by index.
// These platforms group courses under schools/sites, so one must be selected
// before listing courses.
func (a *App) SelectSubgroup(index int) map[string]interface{} {
switch a.activePlatform {
case "teachable":
if index >= 0 && index < len(a.teachable.schools) {
a.teachable.schoolID = jsonStr(a.teachable.schools[index], "id")
return map[string]interface{}{"success": true}
}
case "kajabi":
if index >= 0 && index < len(a.kajabi.sites) {
a.kajabi.siteID = jsonStr(a.kajabi.sites[index], "id")
return map[string]interface{}{"success": true}
}
case "skool":
return map[string]interface{}{"success": false}
}
return map[string]interface{}{"success": false}
}
// platformDir returns the platform-specific subdirectory inside downloadDir.
// Each downloader saves into its own folder (e.g. vintecinco_dl/Hotmart/).
func (a *App) platformDir() string {
sub := ""
switch a.activePlatform {
case "kiwify":
sub = "Kiwify"
case "gumroad":
sub = "Gumroad"
case "hotmart":
sub = "Hotmart"
case "teachable":
sub = "Teachable"
case "kajabi":
sub = "Kajabi"
case "skool":
sub = "Skool"
case "pluralsight":
sub = "Pluralsight"
case "greatcourses":
sub = "The Great Courses"
case "masterclass":
sub = "MasterClass"
case "thinkific":
sub = "Thinkific"
}
if sub == "" {
return a.downloadDir
}
return filepath.Join(a.downloadDir, sub)
}
// emit sends an event to the Wails frontend with the given name and data payload.
// Common events: dl_started, dl_progress, dl_complete, dl_cancelled, dl_error,
// batch_started, batch_progress, batch_done.
func (a *App) emit(event string, data map[string]interface{}) {
runtime.EventsEmit(a.ctx, event, data)
}
// sleepMs pauses execution for the given number of milliseconds.
func sleepMs(ms int) {
time.Sleep(time.Duration(ms) * time.Millisecond)
}
// defaultDownloadDir returns the default folder for saving courses.
// Prefers ~/Downloads/vintecinco_dl, falls back to ~/vintecinco_dl.
func defaultDownloadDir() string {
home, err := os.UserHomeDir()
if err != nil {
return filepath.Join(".", "downloads")
}
downloads := filepath.Join(home, "Downloads")
if _, err := os.Stat(downloads); err == nil {
return filepath.Join(downloads, "vintecinco_dl")
}
return filepath.Join(home, "vintecinco_dl")
}