package main import ( "bytes" "fmt" "github.com/dustin/go-humanize" log "github.com/sirupsen/logrus" "github.com/spf13/viper" "io" "io/ioutil" "net/http" "os" "os/exec" "regexp" "strings" ) func main() { log.Info("Reading config.yaml") viper.AddConfigPath(".") viper.SetConfigName("config") viper.SetConfigType("yaml") err := viper.ReadInConfig() if err != nil { log.Fatalf("Error reading config.yaml: %s", err.Error()) } if viper.GetString("nodeBinPath") == "" { log.Fatal("nodeBinPath not set in config.yaml") } currentPath := os.Getenv("PATH") os.Setenv("PATH", fmt.Sprintf("%s:%s", currentPath, viper.GetString("nodeBinPath"))) log.Info("Finding Latest BluOS Controller") l, err := getLatest() if err != nil { log.Fatalf("Error getting latest client URL: %s", err.Error()) } log.Infof("Downloading Latest BlueOS controller: %s", *l) err = downloadFile("./controller.dmg", *l) if err != nil { log.Fatalf("Error downloading latest controller dmg: %s", err.Error()) } log.Info("Extracting BlueOS Controller DMG") extractController() log.Info("Extracting ASAR") extractAsar() log.Info("Patching electron.js") patchFile( "./bluos/www/js/electron.js", ` if(platform=='mac'){ path = "/Applications/Spotify.app" }`, ` if(platform=='mac'){ path = "/Applications/Spotify.app" } if(platform=='lin64') { path = "/snap/bin/spotify" }`) log.Info("Patching app.js - Update Check") patchFile("./bluos/www/app.js", `f7.checkAppUpdate=function(e){var t;"macOS"==f7.appInfo.platform&&(t="http://upgrade.nadelectronics.com/desktop_app/osx/version.xml?currentVersion"),"Windows"==f7.appInfo.platform&&(t="http://upgrade.nadelectronics.com/desktop_app/windows/version.xml?currentVersion")`, `f7.checkAppUpdate=function(e){var t;"linux" == f7.appInfo.platform && (t = "http://upgrade.nadelectronics.com/desktop_app/osx/version.xml?currentVersion"), "macOS"==f7.appInfo.platform&&(t="http://upgrade.nadelectronics.com/desktop_app/osx/version.xml?currentVersion"),"Windows"==f7.appInfo.platform&&(t="http://upgrade.nadelectronics.com/desktop_app/windows/version.xml?currentVersion")`) log.Info("Patching app.js - Update Platform") patchFile("./bluos/www/app.js", `.autoupgrade,queue:{loading:!1,pagesize:200,total:0}}`, `.autoupgrade,queue:{loading:!1,pagesize:200,total:0},platform: "linux"}`) log.Info("Adding electron dependency") addNpmPackage("electron@^9.0.0") log.Info("Adding electron-builder dependency") addNpmPackage("electron-builder") log.Info("Building Snap and AppImage") buildPackage() log.Info("Cleaning up") cleanUp() } func cleanUp() { os.Rename("./bluos/dist", "./dist") os.RemoveAll("./bluos") os.RemoveAll("./controller") os.Remove("./controller.dmg") } func addNpmPackage(packageName string) error { cmd := exec.Command(fmt.Sprintf("%s/npm", viper.GetString("nodeBinPath")), "install", packageName, "--save-dev") cmd.Dir = "./bluos" var out bytes.Buffer var eout bytes.Buffer cmd.Stdout = &out cmd.Stderr = &eout err := cmd.Run() if err != nil { log.Errorf("Error installing electron: %s", eout.String()) return err } return nil } func buildPackage() error { cmd := exec.Command(fmt.Sprintf("%s/electron-builder", viper.GetString("nodeBinPath"))) cmd.Dir = "./bluos" var out bytes.Buffer var eout bytes.Buffer cmd.Stdout = &out cmd.Stderr = &eout err := cmd.Run() if err != nil { log.Errorf("Error extracting ASAR: %s", eout.String()) return err } return nil } func patchFile(filePath, search, replace string) error { input, err := ioutil.ReadFile(filePath) if err != nil { return err } output := strings.Replace(string(input), search, replace, 1) err = ioutil.WriteFile(filePath, []byte(output), 0) if err != nil { return err } return nil } func extractAsar() error { folders, err := ioutil.ReadDir("./controller/") if err != nil { return err } for _, f := range folders { if strings.Contains(f.Name(), "BluOS Controller") { cmd := exec.Command(fmt.Sprintf("%s/npx", viper.GetString("nodeBinPath")), "asar", "extract", fmt.Sprintf("./controller/%s/BluOS Controller.app/Contents/Resources/app.asar", f.Name()), "./bluos") var out bytes.Buffer var eout bytes.Buffer cmd.Stdout = &out cmd.Stderr = &eout err := cmd.Run() if err != nil { log.Errorf("Error extracting ASAR: %s", eout.String()) return err } } } return nil } func extractController() { c := exec.Command("7z", "x", "-ocontroller", "./controller.dmg") c.Run() } func getLatest() (*string, error) { resp, err := http.Get("https://www.bluesound.com/downloads/") if err != nil { log.Errorf("Error getting Bluesound download page: %s", err.Error()) return nil, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Errorf("Error reading response body") return nil, err } re := regexp.MustCompile(`(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?.dmg`) url := string(re.Find(body)) return &url, nil } type WriteCounter struct { Total uint64 } func (wc *WriteCounter) Write(p []byte) (int, error) { n := len(p) wc.Total += uint64(n) wc.PrintProgress() return n, nil } func (wc WriteCounter) PrintProgress() { // Clear the line by using a character return to go back to the start and remove // the remaining characters by filling it with spaces fmt.Printf("\r%s", strings.Repeat(" ", 35)) // Return again and print current status of download // We use the humanize package to print the bytes in a meaningful way (e.g. 10 MB) fmt.Printf("\rDownloading... %s complete", humanize.Bytes(wc.Total)) } func downloadFile(filepath string, url string) error { // Create the file, but give it a tmp file extension, this means we won't overwrite a // file until it's downloaded, but we'll remove the tmp extension once downloaded. out, err := os.Create(filepath + ".tmp") if err != nil { return err } // Get the data resp, err := http.Get(url) if err != nil { out.Close() return err } defer resp.Body.Close() // Create our progress reporter and pass it to be used alongside our writer counter := &WriteCounter{} if _, err = io.Copy(out, io.TeeReader(resp.Body, counter)); err != nil { out.Close() return err } // The progress use the same line so print a new line once it's finished downloading fmt.Print("\n") // Close the file without defer so it can happen before Rename() out.Close() if err = os.Rename(filepath+".tmp", filepath); err != nil { return err } return nil }