diff --git a/.gitignore b/.gitignore index bceddcf..7a72e4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .env bin *.txt +artifacts +bin \ No newline at end of file diff --git a/.woodpecker/release.yml b/.woodpecker/release.yml index 41e8c8e..80d6072 100644 --- a/.woodpecker/release.yml +++ b/.woodpecker/release.yml @@ -7,13 +7,22 @@ steps: image: golang:1.20-alpine commands: - apk update - - apk add --no-cache make alpine-sdk g++ + - apk add --no-cache make alpine-sdk g++ bash - make build - echo "$${CI_COMMIT_TAG}" - make release + generate-changelog: + image: python:3.11-alpine + commands: + - apk update + - apk add --no-cache make alpine-sdk g++ bash + - make changelog-draft + release: image: woodpeckerci/plugin-gitea-release + when: + event: tag settings: base_url: https://git.iratusmachina.com files: @@ -25,3 +34,4 @@ steps: skip_verify: true target: main checksum: sha256 + note: "./draft_notes.md" diff --git a/Makefile b/Makefile index 9a5f40c..b1c9af1 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,14 @@ release: rm -rf ${BIN_DIR}/tmp cd ${CURRENT_DIR} +.PHONY: changelog-full +changelog-full: + python3 generate_notes.py --full + +.PHONY: changelog-draft +changelog-draft: + python3 generate_notes.py --draft + .PHONY: lint-all lint-all: golangci-lint run --enable-all diff --git a/generate_notes.py b/generate_notes.py new file mode 100644 index 0000000..98fc461 --- /dev/null +++ b/generate_notes.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +""" +This module generates a draft_notes.md to be attached to a Gitea release and +a CHANGELOG.md to be committed into the repo. + +It is the python version of this https://stackoverflow.com/a/46033999 +""" + +import subprocess +import argparse + +def full(): + with open("CHANGELOG.md", "w+") as fw: + try: + # Get all the tags sorted in decreasing order (the -creatordate is what sorts in decreasing order) + tags = subprocess.check_output(["git", "tag", "--sort=-creatordate"], text=True) + tags = [tag for tag in tags.split("\n") if tag] + # Get the remote url + remote_url = subprocess.check_output(["git", "remote", "get-url", "origin"], text=True) + # Remove the first occurence of the word git starting from the end of the string + remote_url = remote_url[0:remote_url.rfind(".git")] + previous_tag = "" + for tag in tags: + if previous_tag: + # Extract the date of the commit + tag_date = subprocess.check_output(["git", "log", "-1", f"--pretty=format:'%ad'", "--date=short", f"{tag}"], text=True) + tag_date = tag_date.replace("'", "") + + # Get each commit of a tag formatted + formatted_lines = subprocess.check_output(["git", "log", f"{tag}...{previous_tag}", f'--pretty=format:"* %s [View]({remote_url}/commits/%H)"'], text=True) + if formatted_lines: + fw.write(f"## {tag} ({tag_date})\n\n") + # Remove merge commits or Changelog commits + lines = "\n".join([line.replace("\"", "") for line in formatted_lines.split("\n") if all(["merge" not in line.lower(), "changelog.md" not in line.lower()])]) + fw.write(lines) + fw.write("\n\n") + previous_tag = tag + except subprocess.CalledProcessError as e: + print(f"Command failed with return code {e.returncode}") + +def draft(): + with open("draft_notes.md", "w+") as fw: + try: + # Get the remote url + remote_url = subprocess.check_output(["git", "remote", "get-url", "origin"], text=True) + # Remove the first occurence of the word git starting from the end of the string + remote_url = remote_url[0:remote_url.rfind(".git")] + + # Get the current and previous tags + tags = subprocess.check_output(["git", "tag", "--sort=creatordate"], text=True) + tags = [tag for tag in tags.split("\n") if tag] + tags.reverse() + current_tag, previous_tag, *_ = tags + formatted_lines = subprocess.check_output(["git", "log", f"{current_tag}...{previous_tag}", f'--pretty=format:"* %s"'], text=True) + lines = "\n".join([line.replace("\"", "") for line in formatted_lines.split("\n") if all(["merge" not in line.lower(), "changelog.md" not in line.lower()])]) + fw.write(lines) + fw.write("\n\n") + fw.write(f"Compare between recent changes: [{previous_tag[1:]}...{current_tag[1:]}]({remote_url}/compare/{previous_tag}...{current_tag})") + except subprocess.CalledProcessError as e: + print(f"Command failed with return code {e.returncode}") + +def run(): + parser = argparse.ArgumentParser(description='Generate changelogs....') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--full", help="Generate full changelog", action="store_true") + group.add_argument("--draft", help="Generate notes with tag", action="store_true") + args = parser.parse_args() + if args.full: + full() + elif args.draft: + draft() + +if __name__ == "__main__": + run() diff --git a/internal/cmdline/cmdline.go b/internal/cmdline/cmdline.go index c0e9372..bd77493 100644 --- a/internal/cmdline/cmdline.go +++ b/internal/cmdline/cmdline.go @@ -2,6 +2,7 @@ package cmdline import ( "bytes" + "errors" "flag" "fmt" "log" @@ -15,6 +16,8 @@ import ( var ( printDebug bool + useGenius bool + useGoogle bool outputFile string errorLog *log.Logger mainLog *log.Logger @@ -29,69 +32,78 @@ func recurseNodes(top *html.Node, sb *strings.Builder) { } } -func searchGoogle(song string) error { +func searchGoogle(song string) (err error) { runOption := &playwright.RunOptions{ SkipInstallBrowsers: true, } - err := playwright.Install(runOption) - if err != nil { - return fmt.Errorf("could not install playwright dependencies: %v", err) + tempErr := playwright.Install(runOption) + if tempErr != nil { + err = fmt.Errorf("could not install playwright dependencies: %v", tempErr) + return err } - pw, err := playwright.Run() - if err != nil { - return fmt.Errorf("could not start playwright: %v", err) + pw, tempErr := playwright.Run() + if tempErr != nil { + err = fmt.Errorf("could not start playwright: %v", tempErr) + return err } - defer func(pw *playwright.Playwright) error { - err := pw.Stop() - if err != nil { - return fmt.Errorf("could not stop Playwright: %v", 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) } - 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) + 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) error { - err = browser.Close() - if err != nil { - return fmt.Errorf("could not close browser: %v", 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) } - return nil }(browser) - page, err := browser.NewPage() - if err != nil { - return fmt.Errorf("could not create page: %v", err) + page, tempErr := browser.NewPage() + if tempErr != nil { + err = fmt.Errorf("could not create page: %v", tempErr) + return err } - if _, err := page.Goto(fmt.Sprintf("https://www.google.com/search?q=%ss+lyrics", song), + if _, tempErr := 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) + }); tempErr != nil { + err = fmt.Errorf("could not goto: %v", tempErr) + return err } - err = page.Locator("body").WaitFor(playwright.LocatorWaitForOptions{ + tempErr = page.Locator("body").WaitFor(playwright.LocatorWaitForOptions{ State: playwright.WaitForSelectorStateVisible, }) - if err != nil { - return fmt.Errorf("could not wait for body: %v", err) + if tempErr != nil { + err = fmt.Errorf("could not wait for body: %v", tempErr) + return err } - html, err := page.Locator("html").InnerHTML() - if err != nil { - return fmt.Errorf("could not get innerHtml: %v", err) + html, tempErr := page.Locator("html").InnerHTML() + if tempErr != nil { + err = fmt.Errorf("could not get innerHtml: %v", tempErr) + return err } - doc, err := htmlquery.Parse(bytes.NewReader([]byte(html))) + doc, tempErr := htmlquery.Parse(bytes.NewReader([]byte(html))) if err != nil { - return fmt.Errorf("could not parse the innerHtml: %v", err) + err = fmt.Errorf("could not parse the innerHtml: %v", tempErr) + return err } - nodes, err := htmlquery.QueryAll(doc, "//div[@data-lyricid]/div") + nodes, tempErr := htmlquery.QueryAll(doc, "//div[@data-lyricid]/div") if err != nil { - return fmt.Errorf("could not get the nodes: %v", err) + err = fmt.Errorf("could not get the nodes: %v", tempErr) + return err } var sb strings.Builder @@ -103,9 +115,10 @@ func searchGoogle(song string) error { 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) + 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...") @@ -128,11 +141,13 @@ func Main() int { 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, " 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, " (b) It then tries to get search for the same song using the \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() @@ -142,8 +157,13 @@ func Main() int { 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") - flags.Parse(os.Args[1:]) + err := flags.Parse(os.Args[1:]) + if err != nil { + return 1 + } if len(flags.Args()) > 1 { errorLog.Println("Error: too many command-line arguments") @@ -159,6 +179,18 @@ func Main() int { 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 @@ -184,10 +216,19 @@ func Main() int { 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 + 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