From cb7c0a0a7374be695986df4d091526af4cb8937d Mon Sep 17 00:00:00 2001 From: objnull Date: Sun, 9 Jun 2024 15:49:00 -0400 Subject: [PATCH] Base Upload --- .gitignore | 4 + go.mod | 9 +++ go.sum | 8 ++ main.go | 101 +++++++++++++++++++++++++ src/cli.go | 49 +++++++++++++ src/filer.go | 62 ++++++++++++++++ src/html.go | 71 ++++++++++++++++++ src/puller.go | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 504 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 src/cli.go create mode 100644 src/filer.go create mode 100644 src/html.go create mode 100644 src/puller.go diff --git a/.gitignore b/.gitignore index adf8f72..9114e40 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +# Program directories +.tmp/** +out/** + # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2ba8fb9 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module SongPuller + +go 1.22.4 + +require ( + github.com/bogem/id3v2 v1.2.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/text v0.16.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..43797a9 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/bogem/id3v2 v1.2.0 h1:hKDF+F1gOgQ5r1QmBCEZUk4MveJbKxCeIDSBU7CQ4oI= +github.com/bogem/id3v2 v1.2.0/go.mod h1:t78PK5AQ56Q47kizpYiV6gtjj3jfxlz87oFpty8DYs8= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..509095c --- /dev/null +++ b/main.go @@ -0,0 +1,101 @@ +// Package +package main + +// Libraries +import ( + backend "SongPuller/src" + "fmt" +) + +// Variables +var pages = [2]backend.CLI{} +var page int = 0 + +// Functions +func initPages() { + // Holders + var cPage backend.CLI + + // Main Menu + /// Creating variable + cPage.Init("Main Menu", func() { + fmt.Println("1) Download a Song") + fmt.Println("2) Exit") + fmt.Print("Select option [1-2]: ") + + // The scan variable + var usrInput string + + // Scanning what the user wants + fmt.Scanln(&usrInput) + + // Based on this, what do we do? + if usrInput == "1" { + page = 1 + } + }) + /// Append page + pages[0] = cPage + + // Song Info + /// Creating variable + cPage.Init("Song Information", func() { + // The scan variable + var usrInput string + var songURL string + + // Introduction + fmt.Println("Before we proceed, Some basic info") + fmt.Println("This program is designed to download songs from:") + fmt.Print(string(backend.CliYellow)) + fmt.Println(" - Soundcloud") + fmt.Print(string(backend.CliWhite)) + fmt.Println() + fmt.Println("We just need you to give us some very basic information.") + fmt.Println() + + // The URL of the site + fmt.Println("First, what's the Share URL of your song?") + fmt.Print("URL [https://www.soundcloud.com/---]: ") + + // Scanning what the user wants + fmt.Scanln(&songURL) + fmt.Println() + + // Running the puller + var meta backend.SongMetadata + meta.Init("soundcloud", songURL) + + // Asking if the song name is correct + fmt.Println("Is your song's name: " + string(backend.CliGreen) + meta.Title + string(backend.CliWhite) + "?") + fmt.Print("Select option [y/n]: ") + + // Scanning if the user likes what is shown + fmt.Scanln(&usrInput) + fmt.Println() + + // If it's not the song they want, then appologize! + if usrInput != "y" && usrInput != "Y" { + fmt.Println() + fmt.Println("Sorry about that! If you could please try that again.") + fmt.Println() + } + + // Switch back to main menu + page = 0 + }) + /// Append page + pages[1] = cPage +} + +// Entry Point +func main() { + // Initilizing pages + initPages() + + // Run Loop + for backend.Running { + // Doing page stuff + pages[page].Run() + } +} diff --git a/src/cli.go b/src/cli.go new file mode 100644 index 0000000..970c542 --- /dev/null +++ b/src/cli.go @@ -0,0 +1,49 @@ +// Package +package backend + +// Libraries +import "fmt" + +// Constants +// / Colors +const CliRed string = "\033[31m" +const CliGreen string = "\033[32m" +const CliYellow string = "\033[33m" +const CliBlue string = "\033[34m" +const CliPurple string = "\033[35m" +const CliCyan string = "\033[36m" +const CliWhite string = "\033[37m" + +// Variables +var Running bool + +// Structs +type CLI struct { + Title string + OnRun func() +} + +// Interfaces +func (c *CLI) Init(title string, onRun func()) { + // Setting variables + Running = true + c.Title = title + c.OnRun = onRun +} +func (c *CLI) Run() { + // Drawing Title + fmt.Print(string(CliCyan)) + fmt.Println(" - " + c.Title + " - ") + fmt.Print(string(CliWhite)) + + // New line + fmt.Println() + + // Running the rest of the draw function + c.OnRun() + + // New lines + for range 2 { + fmt.Println() + } +} diff --git a/src/filer.go b/src/filer.go new file mode 100644 index 0000000..4df9405 --- /dev/null +++ b/src/filer.go @@ -0,0 +1,62 @@ +// Package +package backend + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" +) + +// Libraries + +// Structs +type Filer struct { +} + +// Interfaces +func (f *Filer) DownloadFromURL(url string, path string, filename string) bool { + // Get the image from the URL + resp, err := http.Get(url) + if err != nil { + fmt.Println("FILER: Error fetching image:", err) + return false + } + defer resp.Body.Close() + + // Get the current working directory + cwd, err := os.Getwd() + if err != nil { + fmt.Println("FILER: Error getting current working directory:", err) + return false + } + + // Define the destination directory + destDir := filepath.Join(cwd, path) + + // Create the directory if it doesn't exist + err = os.MkdirAll(filepath.Join(path), os.ModePerm) + if err != nil { + fmt.Println("FILER: Error creating directory:", err) + return false + } + + // Create a file to store the image + file, err := os.Create(filepath.Join(destDir, filename)) + if err != nil { + fmt.Println("FILER: Error creating file:", err) + return false + } + defer file.Close() + + // Copy the image data to the file + _, err = io.Copy(file, resp.Body) + if err != nil { + fmt.Println("FILER: Error copying image data:", err) + return false + } + + // It worked! + return true +} diff --git a/src/html.go b/src/html.go new file mode 100644 index 0000000..b70b957 --- /dev/null +++ b/src/html.go @@ -0,0 +1,71 @@ +// Package +package backend + +import ( + "bytes" + "fmt" + "strings" + + "golang.org/x/net/html" +) + +// Libraries + +// Structs +type HTML_Document struct { + content []byte + doc *html.Node +} + +// Interfaces +func (h *HTML_Document) Init(content []byte) { + // Setting the content + h.content = content + + // Parsing the HTML + var err error + h.doc, err = html.Parse(bytes.NewReader(h.content)) + if err != nil { + fmt.Println("HTML: Error parsing HTML") + return + } +} +func (h *HTML_Document) FindByID(n *html.Node, id string, tag string) *html.Node { + if n.Type == html.ElementNode && n.Data == tag { + for _, attr := range n.Attr { + if attr.Key == "id" && strings.Contains(attr.Val, id) { + return n + } + } + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + if node := h.FindByID(c, id, tag); node != nil { + return node + } + } + return nil +} +func (h *HTML_Document) GetTextContent(node *html.Node) string { + if node == nil { + return "" + } + + var textContent string + if node.Type == html.TextNode { + textContent = node.Data + } + + for c := node.FirstChild; c != nil; c = c.NextSibling { + textContent += h.GetTextContent(c) + } + + return textContent +} +func (h *HTML_Document) GetElementAttr(n *html.Node, attr string) string { + for _, a := range n.Attr { + if a.Key == attr { + return a.Val + } + } + return "" +} diff --git a/src/puller.go b/src/puller.go new file mode 100644 index 0000000..4b85d97 --- /dev/null +++ b/src/puller.go @@ -0,0 +1,200 @@ +// Package +package backend + +// Libraries +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "regexp" + "strings" + + "github.com/bogem/id3v2" +) + +// Structs +type SongMetadata struct { + url string + + Title string + author string + image string + download string +} + +// Functions +func getCSRFTokenAndCookie(url string) (string, *http.Cookie, error) { + client := &http.Client{} + resp, err := client.Get(url) + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + + // Extract CSRF token from cookies + var csrfCookie *http.Cookie + for _, cookie := range resp.Cookies() { + if cookie.Name == "csrftoken" { + csrfCookie = cookie + break + } + } + if csrfCookie == nil { + return "", nil, fmt.Errorf("CSRF cookie not found") + } + + // If the CSRF token is stored in the page content + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", nil, err + } + re := regexp.MustCompile(`name="csrfmiddlewaretoken" value="([^"]+)"`) + match := re.FindStringSubmatch(string(body)) + if len(match) > 1 { + return match[1], csrfCookie, nil + } + + return "", nil, fmt.Errorf("CSRF token not found") +} + +func (s *SongMetadata) pullFromSoundCloud() { + // Reference variables + apiBase := "https://sclouddownloader.net" + apiURL := "https://sclouddownloader.net/download-sound-track" + + // Getting CSRF token + csrfToken, csrfCookie, err := getCSRFTokenAndCookie(apiBase) + if err != nil { + fmt.Println("PULLER: Error fetching CSRF token") + return + } + + // Post data + data := url.Values{} + data.Set("url", s.url) + data.Set("csrfmiddlewaretoken", csrfToken) + + // Creating a POST request + req, err := http.NewRequest("POST", apiURL, bytes.NewBufferString(data.Encode())) + + // Was there an error sending a request? + if err != nil { + fmt.Println("PULLER: Error creating API request") + return + } + + // Setting the request headers + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("X-CSRFTOKEN", csrfToken) + req.Header.Set("Referer", apiURL) + req.AddCookie(csrfCookie) + + // Send request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Println("PULLER: Error sending request to API") + return + } + defer resp.Body.Close() + + // Read response + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println("PULLER: Error reading response") + return + } + + // Feeding the body content into an HTML parser + var htmldoc HTML_Document + htmldoc.Init(body) + + // Now we get specific elements like the title of the song: + s.Title = strings.Split(htmldoc.GetTextContent(htmldoc.FindByID(htmldoc.doc, "trackTitle", "p")), ".mp3")[0] + s.author = strings.Split(htmldoc.GetTextContent(htmldoc.FindByID(htmldoc.doc, "trackUploader", "p")), "by ")[1] + s.download = htmldoc.GetElementAttr(htmldoc.FindByID(htmldoc.doc, "trackLink", "a"), "href") + s.image = htmldoc.GetElementAttr(htmldoc.FindByID(htmldoc.doc, "trackThumbnail", "img"), "src") + + // Downloading the song and image + var filer Filer + + if !filer.DownloadFromURL(s.image, ".tmp", s.Title+".jpg") || !filer.DownloadFromURL(s.download, "out", s.Title+".mp3") { + fmt.Println("PULLER: Exiting process...") + return + } + + // Apply Metadata + s.ApplyMetadata() +} + +func (s *SongMetadata) Init(songPlatform string, url string) { + // Setting variables + s.url = url + + // Doing stuff based on the platform + switch songPlatform { + case "soundcloud": + // Running a Fetch request + s.pullFromSoundCloud() + } +} +func (s *SongMetadata) ApplyMetadata() { + mp3Path := "out/" + s.Title + ".mp3" + imagePath := ".tmp/" + s.Title + ".jpg" + title := s.Title + author := s.author + + // Open the MP3 file + tag, err := id3v2.Open(mp3Path, id3v2.Options{Parse: true}) + if err != nil { + fmt.Println("PULLER: Error opening MP3 file:", err) + return + } + defer tag.Close() + + // Set the title and author + tag.SetTitle(title) + tag.SetArtist(author) + + // Add the image + img, err := os.Open(imagePath) + if err != nil { + fmt.Println("PULLER: Error opening image file:", err) + return + } + defer img.Close() + + imgInfo, err := img.Stat() + if err != nil { + fmt.Println("PULLER: Error getting image info:", err) + return + } + + imgData := make([]byte, imgInfo.Size()) + _, err = img.Read(imgData) + if err != nil { + fmt.Println("PULLER: Error reading image data:", err) + return + } + + picFrame := id3v2.PictureFrame{ + Encoding: id3v2.EncodingUTF8, + MimeType: "image/jpeg", + PictureType: id3v2.PTFrontCover, + Description: "Cover", + Picture: imgData, + } + + tag.AddAttachedPicture(picFrame) + + // Save the changes + if err = tag.Save(); err != nil { + fmt.Println("PULLER: Error saving MP3 file:", err) + return + } + + fmt.Println("Successfully downloaded song") +}