An application to display the albumn cover of the track played in cmus.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 231 lines 4.5 kB view raw
1/* 2Cover-viewer notifies and display album cover art. 3This program is designed to be used with cmus. 4 5Usage: 6 7 cover-viewer --mode [visualizer|notify] 8 9Flags: 10 11 --mode [visualizer|notify] 12 Runs cover-viewer in visualizer or notify. In visualizer mode it displays the 13 album cover of the file read from the socket. In notify, reads cmus status line 14 from stdin and sends the file path to the socket. 15*/ 16package main 17 18import ( 19 "bytes" 20 "flag" 21 "fmt" 22 "io" 23 "log" 24 "net" 25 "os" 26 "strings" 27 28 termimg "github.com/blacktop/go-termimg" 29 "github.com/dhowden/tag" 30) 31 32// cmusKeys is the set of keys that could appear in the cmus status line. 33var cmusKeys = map[string]bool{ 34 "status": true, 35 "file": true, 36 "artist": true, 37 "album": true, 38 "albumartist": true, 39 "title": true, 40 "tracknumber": true, 41 "duration": true, 42} 43 44// lastPath indicates the path of the last file read to display the cover, 45// used to avoid read again the same file again. 46var lastPath string 47 48func main() { 49 log.SetOutput(os.Stderr) 50 log.SetFlags(log.LstdFlags | log.Lshortfile) 51 52 mode := flag.String("mode", "", "visualizer or notify") 53 flag.Parse() 54 55 socketPath := getSocketPath() 56 57 switch *mode { 58 case "visualizer": 59 if err := runVisualizer(socketPath); err != nil { 60 fmt.Fprintln(os.Stderr, "visualize error:", err) 61 os.Exit(1) 62 } 63 case "notify": 64 if err := runNotify(socketPath); err != nil { 65 fmt.Fprintln(os.Stderr, "notify error:", err) 66 os.Exit(1) 67 } 68 default: 69 fmt.Fprintln(os.Stderr, "usage: --mode visualizer|notify") 70 os.Exit(1) 71 } 72} 73 74// getSocketPath gets the socket path 75func getSocketPath() string { 76 if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" { 77 return xdg + "/music-viewer.sock" 78 } 79 return "/tmp/music-viewer.sock" 80} 81 82// runNotify writes in the socket the content of the Stdin, meant to be the status 83// string from cmus. 84func runNotify(socketPath string) error { 85 conn, err := net.Dial("unix", socketPath) 86 if err != nil { 87 log.Println(err) 88 return err 89 90 } 91 defer conn.Close() 92 93 _, err = io.Copy(conn, os.Stdin) 94 return err 95} 96 97// runVisualizer listen to the socket and handles the message with the status of 98// cmus 99func runVisualizer(socketPath string) error { 100 cleanScreen() 101 _ = os.Remove(socketPath) 102 103 ln, err := net.Listen("unix", socketPath) 104 if err != nil { 105 log.Println(err) 106 return err 107 } 108 defer ln.Close() 109 110 for { 111 conn, err := ln.Accept() 112 if err != nil { 113 log.Println(err) 114 continue 115 } 116 handleConn(conn) 117 } 118} 119 120// handleConn handles the socket connection 121func handleConn(conn net.Conn) { 122 defer conn.Close() 123 124 // read the message 125 data, err := io.ReadAll(conn) 126 if err != nil { 127 log.Println(err) 128 return 129 } 130 131 path, ok := extractFilePath(string(data)) 132 if !ok { 133 return 134 } 135 136 if path == lastPath { 137 return 138 } 139 140 showCover(path) 141 lastPath = path 142} 143 144// parseCmusLine reads the status line from cmus and parse it to extract the path to 145// the file 146func parseCmusLine(input string) map[string]string { 147 parts := strings.Fields(input) 148 result := make(map[string]string) 149 150 var currentKey string 151 var buffer []string 152 153 flush := func() { 154 if currentKey != "" && len(buffer) > 0 { 155 result[currentKey] = strings.Join(buffer, " ") 156 } 157 buffer = nil 158 } 159 160 for _, part := range parts { 161 if cmusKeys[part] { 162 flush() 163 currentKey = part 164 continue 165 } 166 buffer = append(buffer, part) 167 } 168 169 flush() 170 return result 171} 172 173// extractFilePath extract the file path from the status string 174func extractFilePath(input string) (string, bool) { 175 data := parseCmusLine(input) 176 path, ok := data["file"] 177 return path, ok 178} 179 180// cleanScreen clean the screen 181func cleanScreen() { 182 fmt.Print("\033[2J") 183 fmt.Print("\033[H") 184 fmt.Print("\n") 185} 186 187// showCover read the picture tag from the file and shows it in kitty terminal 188func showCover(path string) { 189 f, err := os.Open(path) 190 if err != nil { 191 log.Println(err) 192 return 193 } 194 defer f.Close() 195 196 meta, err := tag.ReadFrom(f) 197 if err != nil { 198 log.Println(err) 199 return 200 } 201 202 pic := meta.Picture() 203 if pic == nil { 204 log.Println(err) 205 return 206 } 207 208 renderCover(pic.Data) 209} 210 211// renderCover renders the image in the terminal 212func renderCover(data []byte) { 213 // clean screen 214 cleanScreen() 215 216 // get terminal size 217 features := termimg.QueryTerminalFeatures() 218 219 imgData := bytes.NewReader(data) 220 img, err := termimg.From(imgData) 221 if err != nil { 222 log.Println(err) 223 return 224 } 225 226 img. 227 Width(features.WindowCols). 228 Height(features.WindowRows). 229 Scale(termimg.ScaleFit). 230 Print() 231}