lyricdownloader/internal/cmdline/cmdline.go

246 lines
6.5 KiB
Go

package cmdline
import (
"bytes"
"errors"
"flag"
"fmt"
"log"
"os"
"strings"
"github.com/antchfx/htmlquery"
"github.com/playwright-community/playwright-go"
"golang.org/x/net/html"
)
var (
printDebug bool
useGenius bool
useGoogle bool
outputFile string
errorLog *log.Logger
mainLog *log.Logger
)
func recurseNodes(top *html.Node, sb *strings.Builder) {
if top.Type == html.ElementNode && top.Data == "span" {
sb.WriteString(htmlquery.InnerText(top) + "\n")
}
for c := top.FirstChild; c != nil; c = c.NextSibling {
recurseNodes(c, sb)
}
}
func searchGoogle(song string) (err error) {
runOption := &playwright.RunOptions{
SkipInstallBrowsers: true,
}
tempErr := playwright.Install(runOption)
if tempErr != nil {
err = fmt.Errorf("could not install playwright dependencies: %v", tempErr)
return err
}
pw, tempErr := playwright.Run()
if tempErr != nil {
err = fmt.Errorf("could not start playwright: %v", tempErr)
return err
}
defer func(pw *playwright.Playwright) {
tempErr := pw.Stop()
if tempErr != nil {
e := fmt.Errorf("could not stop Playwright: %v", tempErr)
err = errors.Join(err, e)
}
}(pw)
option := playwright.BrowserTypeLaunchOptions{
Channel: playwright.String("chrome"),
Headless: playwright.Bool(false),
}
browser, tempErr := pw.Chromium.Launch(option)
if tempErr != nil {
err = fmt.Errorf("could not launch browser: %v", tempErr)
return err
}
defer func(browser playwright.Browser) {
tempErr = browser.Close()
if tempErr != nil {
e := fmt.Errorf("could not close browser: %v", tempErr)
err = errors.Join(err, e)
}
}(browser)
page, tempErr := browser.NewPage()
if tempErr != nil {
err = fmt.Errorf("could not create page: %v", tempErr)
return err
}
if _, tempErr := page.Goto(fmt.Sprintf("https://www.google.com/search?q=%ss+lyrics", song),
playwright.PageGotoOptions{
WaitUntil: playwright.WaitUntilStateLoad,
}); tempErr != nil {
err = fmt.Errorf("could not goto: %v", tempErr)
return err
}
tempErr = page.Locator("body").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
})
if tempErr != nil {
err = fmt.Errorf("could not wait for body: %v", tempErr)
return err
}
html, tempErr := page.Locator("html").InnerHTML()
if tempErr != nil {
err = fmt.Errorf("could not get innerHtml: %v", tempErr)
return err
}
doc, tempErr := htmlquery.Parse(bytes.NewReader([]byte(html)))
if err != nil {
err = fmt.Errorf("could not parse the innerHtml: %v", tempErr)
return err
}
nodes, tempErr := htmlquery.QueryAll(doc, "//div[@data-lyricid]/div")
if err != nil {
err = fmt.Errorf("could not get the nodes: %v", tempErr)
return err
}
var sb strings.Builder
for _, node := range nodes {
recurseNodes(node, &sb)
}
if sb.Len() > 0 {
if printDebug {
mainLog.Println("Writing lyrics from Google...")
}
filename := fmt.Sprintf("%s_google.txt", outputFile)
tempErr = os.WriteFile(filename, []byte(sb.String()), os.ModePerm)
if tempErr != nil {
err = fmt.Errorf("could not write to %s: %v", filename, err)
return err
}
} else {
mainLog.Println("Lyrics cannot be found...")
}
return nil
}
// func searchGenius(song string, lg *log.Logger) {
// }
func Main() int {
programName := os.Args[0]
errorLog = log.New(os.Stderr, "", 0)
mainLog = log.New(os.Stdout, "", 0)
flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
flags.Usage = func() {
out := flags.Output()
fmt.Fprintf(out, "Usage: %v \n\n", programName)
fmt.Fprint(out, " This program is used to download lyrics for a song\n")
fmt.Fprint(out, " from the internet. There are two modes of operation.\n\n")
fmt.Fprint(out, " The first mode is scraping the lyrics from Google. So the operation is this: \n")
fmt.Fprint(out, " It first opens a chrome window, searches for the lyrics \n")
fmt.Fprint(out, " , and copies the lyrics returned by Google Search to a \n")
fmt.Fprint(out, " file defined by you.\n\n")
fmt.Fprint(out, " The second mode is getting the lyrics from Genius API. So the operation is this: \n")
fmt.Fprint(out, " It then tries to get search for the same song using the \n")
fmt.Fprint(out, " Genius API. It then tries to compare the lyrics with the \n")
fmt.Fprint(out, " Genius one.\n\n")
flags.PrintDefaults()
}
outputFlag := flags.String("output", "", "Optional. Lyrics filename")
verboseFlag := flags.Bool("verbose", false, "Optional. Turn on debug. Default is false.")
searchFlag := flags.String("search", "", "Required. Name of song to search. If the name of the song is not a single word, put in quotes\"\"")
helpFlag := flags.Bool("help", false, "Optional. Print Usage")
useGoogleFlag := flags.Bool("google", false, "Optional. Use google.")
useGeniusFlag := flags.Bool("genius", false, "Optional. Use genius")
err := flags.Parse(os.Args[1:])
if err != nil {
return 1
}
if len(flags.Args()) > 1 {
errorLog.Println("Error: too many command-line arguments")
flags.Usage()
return 1
}
allSetFlags := flagsSet(flags)
if allSetFlags["help"] && (allSetFlags["output"] || allSetFlags["search"] || allSetFlags["verbose"]) {
errorLog.Println("Error: if -help is set, -output, -search and -verbose must remain unset")
flags.Usage()
return 1
}
if !allSetFlags["google"] && !allSetFlags["genius"] {
errorLog.Println("Error: One of -google or -genius must be set")
flags.Usage()
return 1
}
if allSetFlags["google"] && allSetFlags["genius"] {
errorLog.Println("Error: if -google is set, -genius must remain unset and vice versa")
flags.Usage()
return 1
}
if *helpFlag {
flags.Usage()
return 0
}
songToSearch := *searchFlag
if len(songToSearch) == 0 {
errorLog.Println("Error: the song name must be provided for search")
flags.Usage()
return 1
}
if allSetFlags["output"] {
outputFile = *outputFlag
} else {
mainLog.Printf("Using %s as the name of the file(s) for downloaded lyrics..\n", songToSearch)
outputFile = fmt.Sprintf("%s_lyrics", songToSearch)
}
printDebug = *verboseFlag
if printDebug {
mainLog.Printf("Output flag: %s, Debug flag: %t, Search flag: %s\n", outputFile, printDebug, songToSearch)
}
useGenius = *useGeniusFlag
useGoogle = *useGoogleFlag
if useGoogle {
err := searchGoogle(songToSearch)
if err != nil {
errorLog.Printf("Err: %+v", err)
return 1
}
}
if useGenius {
fmt.Println("No op")
}
return 0
}
// flagsSet returns a set of all the flags what were actually set on the
// command line.
func flagsSet(flags *flag.FlagSet) map[string]bool {
s := make(map[string]bool)
flags.Visit(func(f *flag.Flag) {
s[f.Name] = true
})
return s
}