diff --git a/app.go b/app.go deleted file mode 100644 index 9f5e2c0..0000000 --- a/app.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "context" - "fmt" - "io" - "log" - "os" - "path/filepath" - "regexp" - - "github.com/wader/goutubedl" -) - -var youtubeRegex, _ = regexp.Compile(`^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(?:-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|live\/|v\/)?)([\w\-]+)(\S+)?$`) - -// App struct -type App struct { - ctx context.Context -} - -// NewApp creates a new App application struct -func NewApp() *App { - return &App{} -} - -// startup is called when the app starts. The context is saved -// so we can call the runtime methods -func (a *App) startup(ctx context.Context) { - a.ctx = ctx -} - -// Check if the url is valid youtube video URL -func isValidYouTubeURL(url string) bool { - return youtubeRegex.MatchString(url) -} -func makeOutputDir() { - _, err := os.Stat("./output") - if os.IsNotExist(err) { - err := os.Mkdir("./output", 0755) - if err != nil { - log.Fatal(err) - } - } -} - -// Download the video by the url specified -func (a *App) DownloadVideo(url string) (string, error) { - fmt.Println("url: ", url) - //* check if the url is valid - if !isValidYouTubeURL(url) { - return "Invalid URL", fmt.Errorf("invalid URL") - } - // check if ./bin-deps/yt-dlp.exe exists - if _, err := os.Stat("./bin-deps/yt-dlp.exe"); os.IsNotExist(err) { - GetYtDlp() - } - - var cwd, err = os.Getwd() - goutubedl.Path = filepath.Join(cwd, "bin-deps", "yt-dlp.exe") - - result, err := goutubedl.New(context.Background(), url, goutubedl.Options{}) - - if err != nil { - return "Error getting video", err - } - - downloadResult, err := result.Download(context.Background(), "best") - if err != nil { - return "Error downloading", err - } - - defer downloadResult.Close() - - // todo: Type of video/other options - // todo: audio only - - //save the video - makeOutputDir() - var videoTitle = result.Info.Title - pathToCreate := fmt.Sprintf("%s/output/%s.mp4", cwd, videoTitle) - fmt.Println(pathToCreate) - f, err := os.Create(pathToCreate) - - if err != nil { - log.Fatal(err) - } - defer f.Close() - io.Copy(f, downloadResult) - - return fmt.Sprintf("%s/output/%s.mp4 downloaded", cwd, videoTitle), nil -} diff --git a/backend/controllers/app.go b/backend/controllers/app.go new file mode 100644 index 0000000..17ee852 --- /dev/null +++ b/backend/controllers/app.go @@ -0,0 +1,21 @@ +package controllers + +import ( + "context" +) + +// App struct +type App struct { + ctx context.Context +} + +// NewApp creates a new App application struct +func NewApp() *App { + return &App{} +} + +// startup is called hen the app starts. The context is saved +// so we can call the runtime methods +func (a *App) Startup(ctx context.Context) { + a.ctx = ctx +} diff --git a/backend/controllers/download.go b/backend/controllers/download.go new file mode 100644 index 0000000..3264a0f --- /dev/null +++ b/backend/controllers/download.go @@ -0,0 +1,93 @@ +package controllers + +import ( + "fmt" + "os/exec" + "regexp" + "strings" + "unicode" + + "youtube-downloader-go/backend/models" + + "github.com/wailsapp/wails/v2/pkg/runtime" +) + +var youtubeRegex, _ = regexp.Compile(`^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(?:-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|live\/|v\/)?)([\w\-]+)(\S+)?$`) + +// Check if the url is valid youtube video URL +func isValidYouTubeURL(url string) bool { + return youtubeRegex.MatchString(url) +} + +func slugify(input string) string { + // Remove non-ASCII characters + cleaned := strings.Map(func(r rune) rune { + if r > unicode.MaxASCII || !unicode.IsPrint(r) { + return -1 // remove character + } + return r + }, input) + + // Replace spaces with underscores + cleaned = strings.ReplaceAll(cleaned, " ", "_") + + // Remove characters that are invalid in file names + reg, _ := regexp.Compile(`[^a-zA-Z0-9._-]`) + cleaned = reg.ReplaceAllString(cleaned, "") + + // Ensure the filename is not empty + if len(cleaned) == 0 { + cleaned = "default_filename" + } + + return cleaned +} + +// Download the video by the url specified +func (a *App) DownloadVideo(opts models.DownloadOptions) (string, error) { + //* check if the url is valid + + var url = opts.URL + + if !isValidYouTubeURL(url) { + err := fmt.Errorf("invalid url: %s", url) + return err.Error(), err + } + + pathToSave, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{Title: "Save File"}) + + if err != nil { + return err.Error(), err + } + + cmdArgs := []string{"-o", pathToSave, url} + + // Set download type + if opts.DownloadType == "video" { + cmdArgs = append(cmdArgs, "--format", fmt.Sprintf("bestvideo[ext=%s]", opts.FileType)) + } else if opts.DownloadType == "audio" { + cmdArgs = append(cmdArgs, "--format", fmt.Sprintf("bestaudio[ext=%s]", opts.FileType)) + } else { + err := fmt.Errorf("invalid download type: %s", opts.DownloadType) + return err.Error(), err + } + + // Set resolution and quality options if provided + if opts.Resolution != "" { + cmdArgs = append(cmdArgs, "--format", fmt.Sprintf("%s+%s", opts.Resolution, opts.Quality)) + } + + // Execute yt-dlp command + cmd := exec.Command("yt-dlp", cmdArgs...) + output, err := cmd.CombinedOutput() + if err != nil { + err := fmt.Errorf("error executing yt-dlp: %v\nOutput: %s", err, string(output)) + return err.Error(), err + } + + // Print useful information + fmt.Printf("Downloaded %s from %s to %s\n", opts.DownloadType, url, pathToSave) + fmt.Println(string(output)) + + return "Done downloading", nil +} diff --git a/backend/models/download_options.go b/backend/models/download_options.go new file mode 100644 index 0000000..e4d736b --- /dev/null +++ b/backend/models/download_options.go @@ -0,0 +1,9 @@ +package models + +type DownloadOptions struct { + URL string // e.g., "https://www.youtube.com/watch?v=1234" + DownloadType string // "video" or "audio" + FileType string // e.g., "mp4", "mp3" + Resolution string // e.g., "720p", "1080p" + Quality string // e.g., "best", "worst" +} diff --git a/get_ytdlp.go b/backend/utils/get_ytdlp.go similarity index 60% rename from get_ytdlp.go rename to backend/utils/get_ytdlp.go index 6c91e89..98c55f5 100644 --- a/get_ytdlp.go +++ b/backend/utils/get_ytdlp.go @@ -1,16 +1,19 @@ -package main +package utils import ( "fmt" "io" "net/http" "os" + "path/filepath" + + "github.com/wader/goutubedl" ) const YtDlpGitgubRelease = "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe" // If yt-dlp isn't installed yet, download it locally. -func GetYtDlp() { +func getYtDlp() { folderPath := "./bin-deps" // Check if the folder exists @@ -39,3 +42,19 @@ func GetYtDlp() { io.Copy(out, resp.Body) } + +// Get yt-dlp if not exists, returns cwd and path of yt-dlp +func YtDlSetup() (string, string) { + // check if ./bin-deps/yt-dlp.exe exists + if _, err := os.Stat("./bin-deps/yt-dlp.exe"); os.IsNotExist(err) { + getYtDlp() + } + + var cwd, err = os.Getwd() + if err != nil { + panic(err) + } + goutubedl.Path = filepath.Join(cwd, "bin-deps", "yt-dlp.exe") + + return cwd, goutubedl.Path +} diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json index b77f214..e114132 100644 --- a/frontend/jsconfig.json +++ b/frontend/jsconfig.json @@ -8,7 +8,6 @@ * a value or a type, so tell TypeScript to enforce using * `import type` instead of `import` for Types. */ - "importsNotUsedAsValues": "error", "isolatedModules": true, "ignoreDeprecations": "5.0", "resolveJsonModule": true, diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index dc8129a..a14aba6 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1,13 +1,29 @@ @@ -16,8 +32,9 @@

{text}

- +
+ diff --git a/main.go b/main.go index f3c6e92..b9c1a0c 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,8 @@ import ( "fmt" "os" + "youtube-downloader-go/backend/controllers" + "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" @@ -17,7 +19,7 @@ func main() { fmt.Println(os.Getwd()) // Create an instance of the app structure - app := NewApp() + app := controllers.NewApp() // Create application with options err := wails.Run(&options.App{ @@ -27,8 +29,7 @@ func main() { AssetServer: &assetserver.Options{ Assets: assets, }, - BackgroundColour: &options.RGBA{R: 64, G: 1, B: 43, A: 1}, - OnStartup: app.startup, + OnStartup: app.Startup, Bind: []interface{}{ app, },