package cmdline import ( "bytes" "flag" "fmt" "log" "os" "strings" "github.com/antchfx/htmlquery" "github.com/playwright-community/playwright-go" "golang.org/x/net/html" ) var ( printDebug 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) error { runOption := &playwright.RunOptions{ SkipInstallBrowsers: true, } err := playwright.Install(runOption) if err != nil { return fmt.Errorf("could not install playwright dependencies: %v", err) } pw, err := playwright.Run() if err != nil { return fmt.Errorf("could not start playwright: %v", err) } defer func(pw *playwright.Playwright) error { err := pw.Stop() if err != nil { return fmt.Errorf("could not stop Playwright: %v", err) } return nil }(pw) option := playwright.BrowserTypeLaunchOptions{ Channel: playwright.String("chrome"), Headless: playwright.Bool(false), } browser, err := pw.Chromium.Launch(option) if err != nil { return fmt.Errorf("could not launch browser: %v", err) } defer func(browser playwright.Browser) error { err = browser.Close() if err != nil { return fmt.Errorf("could not close browser: %v", err) } return nil }(browser) page, err := browser.NewPage() if err != nil { return fmt.Errorf("could not create page: %v", err) } if _, err := page.Goto(fmt.Sprintf("https://www.google.com/search?q=%ss+lyrics", song), playwright.PageGotoOptions{ WaitUntil: playwright.WaitUntilStateLoad, }); err != nil { return fmt.Errorf("could not goto: %v", err) } err = page.Locator("body").WaitFor(playwright.LocatorWaitForOptions{ State: playwright.WaitForSelectorStateVisible, }) if err != nil { return fmt.Errorf("could not wait for body: %v", err) } html, err := page.Locator("html").InnerHTML() if err != nil { return fmt.Errorf("could not get innerHtml: %v", err) } doc, err := htmlquery.Parse(bytes.NewReader([]byte(html))) if err != nil { return fmt.Errorf("could not parse the innerHtml: %v", err) } nodes, err := htmlquery.QueryAll(doc, "//div[@data-lyricid]/div") if err != nil { return fmt.Errorf("could not get the nodes: %v", 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) err = os.WriteFile(filename, []byte(sb.String()), os.ModePerm) if err != nil { return fmt.Errorf("could not write to %s: %v", filename, 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. The steps of operation are shown here: \n\n") fmt.Fprint(out, " (a) 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, " (b) 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") flags.Parse(os.Args[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 *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) } err := searchGoogle(songToSearch) if err != nil { errorLog.Printf("Err: %+v", err) return 1 } 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 }