feature/add-genius #5
			
				
			
		
		
		
	| 
						 | 
					@ -0,0 +1,54 @@
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// isFile - check if fp is a valid file
 | 
				
			||||||
 | 
					func isFile(fp string) bool {
 | 
				
			||||||
 | 
						info, err := os.Stat(fp)
 | 
				
			||||||
 | 
						if os.IsNotExist(err) || !info.Mode().IsRegular() {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getConfig - parse config json file
 | 
				
			||||||
 | 
					func getConfig(filepath string) (Config, error) {
 | 
				
			||||||
 | 
						// if filepath is not empty
 | 
				
			||||||
 | 
						// try to load the token from the file
 | 
				
			||||||
 | 
						// else load it from user homedir
 | 
				
			||||||
 | 
						var config Config
 | 
				
			||||||
 | 
						configFilePath := filepath
 | 
				
			||||||
 | 
						ok := isFile(filepath)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							if printDebug {
 | 
				
			||||||
 | 
								mainLog.Println("Loading config from home directory...")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							dirname, err := os.UserConfigDir()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return config, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							configFilePath := path.Join(dirname, "lyricdownloader/config.json")
 | 
				
			||||||
 | 
							ok := isFile(configFilePath)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return config, fmt.Errorf("%s/lyricdownloader/config.json file is not found", dirname)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						configFile, err := os.Open(configFilePath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return config, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer configFile.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = json.NewDecoder(configFile).Decode(&config)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return config, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return config, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										12
									
								
								go.mod
								
								
								
								
							| 
						 | 
					@ -4,17 +4,27 @@ go 1.20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
	github.com/antchfx/htmlquery v1.3.0
 | 
						github.com/antchfx/htmlquery v1.3.0
 | 
				
			||||||
	github.com/joho/godotenv v1.5.1
 | 
						github.com/gocolly/colly/v2 v2.1.0
 | 
				
			||||||
	github.com/playwright-community/playwright-go v0.4001.0
 | 
						github.com/playwright-community/playwright-go v0.4001.0
 | 
				
			||||||
	golang.org/x/net v0.17.0
 | 
						golang.org/x/net v0.17.0
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
 | 
						github.com/PuerkitoBio/goquery v1.5.1 // indirect
 | 
				
			||||||
 | 
						github.com/andybalholm/cascadia v1.2.0 // indirect
 | 
				
			||||||
 | 
						github.com/antchfx/xmlquery v1.2.4 // indirect
 | 
				
			||||||
	github.com/antchfx/xpath v1.2.4 // indirect
 | 
						github.com/antchfx/xpath v1.2.4 // indirect
 | 
				
			||||||
	github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
 | 
						github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
 | 
				
			||||||
	github.com/go-jose/go-jose/v3 v3.0.1 // indirect
 | 
						github.com/go-jose/go-jose/v3 v3.0.1 // indirect
 | 
				
			||||||
	github.com/go-stack/stack v1.8.1 // indirect
 | 
						github.com/go-stack/stack v1.8.1 // indirect
 | 
				
			||||||
 | 
						github.com/gobwas/glob v0.2.3 // indirect
 | 
				
			||||||
	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 | 
						github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 | 
				
			||||||
 | 
						github.com/golang/protobuf v1.4.2 // indirect
 | 
				
			||||||
 | 
						github.com/kennygrant/sanitize v1.2.4 // indirect
 | 
				
			||||||
 | 
						github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
 | 
				
			||||||
 | 
						github.com/temoto/robotstxt v1.1.1 // indirect
 | 
				
			||||||
	go.uber.org/multierr v1.11.0 // indirect
 | 
						go.uber.org/multierr v1.11.0 // indirect
 | 
				
			||||||
	golang.org/x/text v0.13.0 // indirect
 | 
						golang.org/x/text v0.13.0 // indirect
 | 
				
			||||||
 | 
						google.golang.org/appengine v1.6.6 // indirect
 | 
				
			||||||
 | 
						google.golang.org/protobuf v1.24.0 // indirect
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										101
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										101
									
								
								go.sum
								
								
								
								
							| 
						 | 
					@ -1,47 +1,115 @@
 | 
				
			||||||
 | 
					cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 | 
				
			||||||
 | 
					github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 | 
				
			||||||
 | 
					github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
 | 
				
			||||||
 | 
					github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
 | 
				
			||||||
 | 
					github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
 | 
				
			||||||
 | 
					github.com/andybalholm/cascadia v1.2.0 h1:vuRCkM5Ozh/BfmsaTm26kbjm0mIOM3yS5Ek/F5h18aE=
 | 
				
			||||||
 | 
					github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
 | 
				
			||||||
 | 
					github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0=
 | 
				
			||||||
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
 | 
					github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E=
 | 
				
			||||||
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
 | 
					github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
 | 
				
			||||||
 | 
					github.com/antchfx/xmlquery v1.2.4 h1:T/SH1bYdzdjTMoz2RgsfVKbM5uWh3gjDYYepFqQmFv4=
 | 
				
			||||||
 | 
					github.com/antchfx/xmlquery v1.2.4/go.mod h1:KQQuESaxSlqugE2ZBcM/qn+ebIpt+d+4Xx7YcSGAIrM=
 | 
				
			||||||
 | 
					github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
 | 
				
			||||||
 | 
					github.com/antchfx/xpath v1.1.8/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
 | 
				
			||||||
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
 | 
					github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
 | 
				
			||||||
github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY=
 | 
					github.com/antchfx/xpath v1.2.4 h1:dW1HB/JxKvGtJ9WyVGJ0sIoEcqftV3SqIstujI+B9XY=
 | 
				
			||||||
github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
 | 
					github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
 | 
				
			||||||
 | 
					github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 | 
				
			||||||
 | 
					github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 | 
				
			||||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
 | 
					github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
 | 
				
			||||||
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
 | 
					github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
 | 
				
			||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
					github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
					github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
				
			||||||
 | 
					github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
 | 
					github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 | 
				
			||||||
 | 
					github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 | 
				
			||||||
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
 | 
					github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
 | 
				
			||||||
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 | 
					github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 | 
				
			||||||
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
 | 
					github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
 | 
				
			||||||
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
 | 
					github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
 | 
				
			||||||
 | 
					github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 | 
				
			||||||
 | 
					github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 | 
				
			||||||
 | 
					github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
 | 
				
			||||||
 | 
					github.com/gocolly/colly/v2 v2.1.0 h1:k0DuZkDoCsx51bKpRJNEmcxcp+W5N8ziuwGaSDuFoGs=
 | 
				
			||||||
 | 
					github.com/gocolly/colly/v2 v2.1.0/go.mod h1:I2MuhsLjQ+Ex+IzK3afNS8/1qP3AedHOusRPcRdC5o0=
 | 
				
			||||||
 | 
					github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 | 
				
			||||||
 | 
					github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 | 
				
			||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 | 
					github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 | 
				
			||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 | 
					github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 | 
				
			||||||
 | 
					github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
				
			||||||
 | 
					github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
 | 
				
			||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
					github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
				
			||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 | 
					github.com/jawher/mow.cli v1.1.0/go.mod h1:aNaQlc7ozF3vw6IJ2dHjp2ZFiA4ozMIYY6PyuRJwlUg=
 | 
				
			||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 | 
					github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
 | 
				
			||||||
 | 
					github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
 | 
				
			||||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
 | 
					github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
 | 
				
			||||||
github.com/playwright-community/playwright-go v0.4001.0 h1:2cBiTIjCvFu7zUrZ48C0YC2DIp90Tbudueq4brUGjHM=
 | 
					github.com/playwright-community/playwright-go v0.4001.0 h1:2cBiTIjCvFu7zUrZ48C0YC2DIp90Tbudueq4brUGjHM=
 | 
				
			||||||
github.com/playwright-community/playwright-go v0.4001.0/go.mod h1:quEkYFrvvpQyGSxBjnYbGS52vrUDB2uaY1cOzkkSHCc=
 | 
					github.com/playwright-community/playwright-go v0.4001.0/go.mod h1:quEkYFrvvpQyGSxBjnYbGS52vrUDB2uaY1cOzkkSHCc=
 | 
				
			||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
					github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
				
			||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
					github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
				
			||||||
 | 
					github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 | 
				
			||||||
 | 
					github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
 | 
				
			||||||
 | 
					github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
 | 
				
			||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
					github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
				
			||||||
 | 
					github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
				
			||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
					github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
				
			||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 | 
					github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 | 
				
			||||||
 | 
					github.com/temoto/robotstxt v1.1.1 h1:Gh8RCs8ouX3hRSxxK7B1mO5RFByQ4CmJZDwgom++JaA=
 | 
				
			||||||
 | 
					github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
 | 
				
			||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 | 
					github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 | 
				
			||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 | 
					go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 | 
				
			||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 | 
					go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
					golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
					golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
					golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
				
			||||||
 | 
					golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 | 
				
			||||||
 | 
					golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 | 
				
			||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 | 
					golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
				
			||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
					golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 | 
				
			||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
					golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
				
			||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
					golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
				
			||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 | 
					golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 | 
				
			||||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
 | 
					golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
 | 
				
			||||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
 | 
					golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
 | 
				
			||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 | 
					golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 | 
				
			||||||
 | 
					golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
					golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
					golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
					golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
					golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
					golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
| 
						 | 
					@ -51,16 +119,45 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
 | 
				
			||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
					golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
				
			||||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
 | 
					golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
 | 
				
			||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
					golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
				
			||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
					golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
				
			||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 | 
					golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 | 
				
			||||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 | 
					golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 | 
				
			||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
 | 
					golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
 | 
				
			||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 | 
					golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 | 
				
			||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
					golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 | 
				
			||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
					golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 | 
				
			||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 | 
					golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 | 
				
			||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
					golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
				
			||||||
 | 
					golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 | 
				
			||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
					golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
				
			||||||
 | 
					google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 | 
				
			||||||
 | 
					google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 | 
				
			||||||
 | 
					google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
 | 
				
			||||||
 | 
					google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 | 
				
			||||||
 | 
					google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 | 
				
			||||||
 | 
					google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
 | 
				
			||||||
 | 
					google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
 | 
				
			||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
					gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
				
			||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
					gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
				
			||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
					gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
				
			||||||
 | 
					honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 | 
				
			||||||
 | 
					honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,245 +0,0 @@
 | 
				
			||||||
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
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										152
									
								
								main.go
								
								
								
								
							
							
						
						
									
										152
									
								
								main.go
								
								
								
								
							| 
						 | 
					@ -1,11 +1,10 @@
 | 
				
			||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"flag"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"lyricdownloader/internal/cmdline"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
					 | 
				
			||||||
	_ "github.com/joho/godotenv/autoload"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Possible algorithm
 | 
					// Possible algorithm
 | 
				
			||||||
| 
						 | 
					@ -15,12 +14,147 @@ import (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Possible flags -o (output) -versbose (print all debug) -search (the actual song you want to search)
 | 
					// Possible flags -o (output) -versbose (print all debug) -search (the actual song you want to search)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var geniusApiToken = os.Getenv("GENIUS_API_TOKEN")
 | 
					var (
 | 
				
			||||||
 | 
						printDebug bool
 | 
				
			||||||
 | 
						useGenius  bool
 | 
				
			||||||
 | 
						useGoogle  bool
 | 
				
			||||||
 | 
						outputFile string
 | 
				
			||||||
 | 
						errorLog   *log.Logger
 | 
				
			||||||
 | 
						mainLog    *log.Logger
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func start() 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")
 | 
				
			||||||
 | 
						configFileFlag := flags.String("configFile", "", "Optional. Use with genius")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := flags.Parse(os.Args[1:])
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							errorLog.Printf("Err: %+v", err)
 | 
				
			||||||
 | 
							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 allSetFlags["google"] && allSetFlags["configFile"] {
 | 
				
			||||||
 | 
							errorLog.Println("Error: if -google is set, -configFile 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 {
 | 
				
			||||||
 | 
							configFile := *configFileFlag
 | 
				
			||||||
 | 
							config, err := getConfig(configFile)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								errorLog.Printf("Err: %+v", err)
 | 
				
			||||||
 | 
								return 1
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							GENIUS_API_TOKEN = config.GeniusApiToken
 | 
				
			||||||
 | 
							err = searchGenius(songToSearch)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								errorLog.Printf("Err: %+v", err)
 | 
				
			||||||
 | 
								return 1
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
 | 
						os.Exit(start())
 | 
				
			||||||
	fmt.Println(geniusApiToken)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	os.Exit(cmdline.Main())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,75 @@
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GeniusSearchResponseHit - the path of the response from the Genuis Search Api
 | 
				
			||||||
 | 
					// that contains all the relevant information
 | 
				
			||||||
 | 
					type GeniusSearchResponseHit struct {
 | 
				
			||||||
 | 
						AnnotationCount         int    `json:"annotation_count"`
 | 
				
			||||||
 | 
						APIPath                 string `json:"api_path"`
 | 
				
			||||||
 | 
						ArtistNames             string `json:"artist_names"`
 | 
				
			||||||
 | 
						FullTitle               string `json:"full_title"`
 | 
				
			||||||
 | 
						HeaderImageThumbnailURL string `json:"header_image_thumbnail_url"`
 | 
				
			||||||
 | 
						HeaderImageURL          string `json:"header_image_url"`
 | 
				
			||||||
 | 
						ID                      int    `json:"id"`
 | 
				
			||||||
 | 
						LyricsOwnerID           int    `json:"lyrics_owner_id"`
 | 
				
			||||||
 | 
						LyricsState             string `json:"lyrics_state"`
 | 
				
			||||||
 | 
						Path                    string `json:"path"`
 | 
				
			||||||
 | 
						PyongsCount             any    `json:"pyongs_count"`
 | 
				
			||||||
 | 
						RelationshipsIndexURL   string `json:"relationships_index_url"`
 | 
				
			||||||
 | 
						ReleaseDateComponents   struct {
 | 
				
			||||||
 | 
							Year  int `json:"year"`
 | 
				
			||||||
 | 
							Month int `json:"month"`
 | 
				
			||||||
 | 
							Day   int `json:"day"`
 | 
				
			||||||
 | 
						} `json:"release_date_components"`
 | 
				
			||||||
 | 
						ReleaseDateForDisplay                     string `json:"release_date_for_display"`
 | 
				
			||||||
 | 
						ReleaseDateWithAbbreviatedMonthForDisplay string `json:"release_date_with_abbreviated_month_for_display"`
 | 
				
			||||||
 | 
						SongArtImageThumbnailURL                  string `json:"song_art_image_thumbnail_url"`
 | 
				
			||||||
 | 
						SongArtImageURL                           string `json:"song_art_image_url"`
 | 
				
			||||||
 | 
						Stats                                     struct {
 | 
				
			||||||
 | 
							UnreviewedAnnotations int  `json:"unreviewed_annotations"`
 | 
				
			||||||
 | 
							Hot                   bool `json:"hot"`
 | 
				
			||||||
 | 
							Pageviews             int  `json:"pageviews"`
 | 
				
			||||||
 | 
						} `json:"stats"`
 | 
				
			||||||
 | 
						Title             string `json:"title"`
 | 
				
			||||||
 | 
						TitleWithFeatured string `json:"title_with_featured"`
 | 
				
			||||||
 | 
						URL               string `json:"url"`
 | 
				
			||||||
 | 
						FeaturedArtists   []struct {
 | 
				
			||||||
 | 
							APIPath        string `json:"api_path"`
 | 
				
			||||||
 | 
							HeaderImageURL string `json:"header_image_url"`
 | 
				
			||||||
 | 
							ID             int    `json:"id"`
 | 
				
			||||||
 | 
							ImageURL       string `json:"image_url"`
 | 
				
			||||||
 | 
							IsMemeVerified bool   `json:"is_meme_verified"`
 | 
				
			||||||
 | 
							IsVerified     bool   `json:"is_verified"`
 | 
				
			||||||
 | 
							Name           string `json:"name"`
 | 
				
			||||||
 | 
							URL            string `json:"url"`
 | 
				
			||||||
 | 
						} `json:"featured_artists"`
 | 
				
			||||||
 | 
						PrimaryArtist struct {
 | 
				
			||||||
 | 
							APIPath        string `json:"api_path"`
 | 
				
			||||||
 | 
							HeaderImageURL string `json:"header_image_url"`
 | 
				
			||||||
 | 
							ID             int    `json:"id"`
 | 
				
			||||||
 | 
							ImageURL       string `json:"image_url"`
 | 
				
			||||||
 | 
							IsMemeVerified bool   `json:"is_meme_verified"`
 | 
				
			||||||
 | 
							IsVerified     bool   `json:"is_verified"`
 | 
				
			||||||
 | 
							Name           string `json:"name"`
 | 
				
			||||||
 | 
							URL            string `json:"url"`
 | 
				
			||||||
 | 
						} `json:"primary_artist"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Response for Genuis Search Api - Generated using https://mholt.github.io/json-to-go/
 | 
				
			||||||
 | 
					type GeniusSearchOtherResponse struct {
 | 
				
			||||||
 | 
						Meta struct {
 | 
				
			||||||
 | 
							Status int `json:"status"`
 | 
				
			||||||
 | 
						} `json:"meta"`
 | 
				
			||||||
 | 
						Response struct {
 | 
				
			||||||
 | 
							Hits []struct {
 | 
				
			||||||
 | 
								Highlights []any                   `json:"highlights"`
 | 
				
			||||||
 | 
								Index      string                  `json:"index"`
 | 
				
			||||||
 | 
								Type       string                  `json:"type"`
 | 
				
			||||||
 | 
								Result     GeniusSearchResponseHit `json:"result"`
 | 
				
			||||||
 | 
							} `json:"hits"`
 | 
				
			||||||
 | 
						} `json:"response"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Config is the representation for the Genius API token
 | 
				
			||||||
 | 
					type Config struct {
 | 
				
			||||||
 | 
						GeniusApiToken string `json:"genius_api_token"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,156 @@
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/httputil"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/antchfx/htmlquery"
 | 
				
			||||||
 | 
						"github.com/gocolly/colly/v2"
 | 
				
			||||||
 | 
						"golang.org/x/net/html"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						client           *http.Client = &http.Client{Timeout: 10 * time.Second}
 | 
				
			||||||
 | 
						genuisAPI                     = "https://api.genius.com/search"
 | 
				
			||||||
 | 
						GENIUS_API_TOKEN              = ""
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// makeRequest - generic function for making Http request
 | 
				
			||||||
 | 
					func makeRequest(endpoint string, apiToken string) (GeniusSearchOtherResponse, http.Header, error) {
 | 
				
			||||||
 | 
						var geniusApiResponse GeniusSearchOtherResponse
 | 
				
			||||||
 | 
						req, _ := http.NewRequest(http.MethodGet, endpoint, nil)
 | 
				
			||||||
 | 
						bearerHeader := fmt.Sprintf("Bearer %s", apiToken)
 | 
				
			||||||
 | 
						req.Header.Add("Authorization", bearerHeader)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						reqDump, err := httputil.DumpRequestOut(req, true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return geniusApiResponse, nil, fmt.Errorf("error printing request: %+v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if printDebug {
 | 
				
			||||||
 | 
							mainLog.Printf("REQUEST:\n%s\n", string(reqDump))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err := client.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return geniusApiResponse, nil, fmt.Errorf("error receiving response: %+v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						respDump, err := httputil.DumpResponse(resp, true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return geniusApiResponse, nil, fmt.Errorf("error printing response: %+v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if printDebug {
 | 
				
			||||||
 | 
							mainLog.Printf("RESPONSE:\n%s\n", string(respDump))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = json.NewDecoder(resp.Body).Decode(&geniusApiResponse)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return geniusApiResponse, nil, fmt.Errorf("error decoding response: %+v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if printDebug {
 | 
				
			||||||
 | 
							mainLog.Printf("Decoded => %+v\n", geniusApiResponse)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return geniusApiResponse, resp.Header, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getLyricUrl - get the url of the html page of the song to scrape
 | 
				
			||||||
 | 
					func getLyricUrl(searchTerm string) (string, error) {
 | 
				
			||||||
 | 
						ret, _, err := makeRequest(fmt.Sprintf("%s?q=%s", genuisAPI, searchTerm), GENIUS_API_TOKEN)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(ret.Response.Hits) == 0 {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("there is no song on the Genius Api for the song: %s", searchTerm)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var songs []GeniusSearchResponseHit
 | 
				
			||||||
 | 
						for _, item := range ret.Response.Hits {
 | 
				
			||||||
 | 
							if item.Type == "song" {
 | 
				
			||||||
 | 
								songs = append(songs, item.Result)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Extract the first one as that is most of the time the correct one
 | 
				
			||||||
 | 
						return songs[0].URL, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// stripUnneccessaryChars - unescape all escaped characters
 | 
				
			||||||
 | 
					func stripUnneccessaryChars(nodeHtml string) string {
 | 
				
			||||||
 | 
						captureBr := regexp.MustCompile("<br/>")
 | 
				
			||||||
 | 
						// Replace all br with a new line
 | 
				
			||||||
 | 
						convertedBr := captureBr.ReplaceAllString(nodeHtml, "\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						captureAngleBrackets := regexp.MustCompile(`(?i)\<(.*?)\>`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Remove all content between the angle brackets and the angle brackets themselves
 | 
				
			||||||
 | 
						removedAngleBrackets := captureAngleBrackets.ReplaceAllString(convertedBr, "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Unescape all escaped string
 | 
				
			||||||
 | 
						return html.UnescapeString(removedAngleBrackets)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// searchGenius - print to file scraped song from Genius Search API
 | 
				
			||||||
 | 
					func searchGenius(searchTerm string) error {
 | 
				
			||||||
 | 
						var errs error
 | 
				
			||||||
 | 
						lyricUrl, err := getLyricUrl(searchTerm)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return errors.Join(err, errs)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if printDebug {
 | 
				
			||||||
 | 
							mainLog.Printf("lyricUrl: %s", lyricUrl)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var sb strings.Builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c := colly.NewCollector()
 | 
				
			||||||
 | 
						c.OnHTML("body", func(e *colly.HTMLElement) {
 | 
				
			||||||
 | 
							doc, errT := htmlquery.Parse(bytes.NewReader(e.Response.Body))
 | 
				
			||||||
 | 
							if errT != nil {
 | 
				
			||||||
 | 
								errs = fmt.Errorf("error parsing html : %+v", errT)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// fmt.Println("doc: ", htmlquery.OutputHTML(doc, true))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Use XPath to find nodes
 | 
				
			||||||
 | 
							nodes, errT := htmlquery.QueryAll(doc, "//div[contains(@class, 'Lyrics__Container')]") // Example XPath query
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								errs = fmt.Errorf("error in query: %+v", errT)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, node := range nodes {
 | 
				
			||||||
 | 
								sb.WriteString(stripUnneccessaryChars(htmlquery.OutputHTML(node, true)))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						err = c.Visit(lyricUrl)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return errors.Join(err, errs)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filename := fmt.Sprintf("%s_genius.txt", searchTerm)
 | 
				
			||||||
 | 
						err = os.WriteFile(filename, []byte(sb.String()), os.ModePerm)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							g := fmt.Errorf("could not write to %s: %v", filename, err)
 | 
				
			||||||
 | 
							return errors.Join(errs, g)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return errs
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,119 @@
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/antchfx/htmlquery"
 | 
				
			||||||
 | 
						"github.com/playwright-community/playwright-go"
 | 
				
			||||||
 | 
						"golang.org/x/net/html"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// recurseNodes - walk HtmlNodes of a Html document
 | 
				
			||||||
 | 
					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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// searchGenius - print to file scraped song from google search
 | 
				
			||||||
 | 
					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 tempErr != nil {
 | 
				
			||||||
 | 
							err = fmt.Errorf("could not parse the innerHtml: %v", tempErr)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						nodes, tempErr := htmlquery.QueryAll(doc, "//div[@data-lyricid]/div")
 | 
				
			||||||
 | 
						if tempErr != 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue