File: id3pic.go 1 /* 2 The MIT License (MIT) 3 4 Copyright © 2020-2025 pacman64 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy of 7 this software and associated documentation files (the “Software”), to deal 8 in the Software without restriction, including without limitation the rights to 9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 of the Software, and to permit persons to whom the Software is furnished to do 11 so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 SOFTWARE. 23 */ 24 25 /* 26 Single-file source-code for id3pic. 27 28 To compile a smaller-sized command-line app, you can use the `go` command as 29 follows: 30 31 go build -ldflags "-s -w" -trimpath id3pic.go 32 */ 33 34 package main 35 36 import ( 37 "bufio" 38 "encoding/binary" 39 "errors" 40 "io" 41 "os" 42 ) 43 44 const info = ` 45 id3pic [options...] [file...] 46 47 Extract picture/thumbnail bytes from ID3/MP3 metadata, if available. 48 49 All (optional) leading options start with either single or double-dash: 50 51 -h show this help message 52 -help show this help message 53 ` 54 55 // errNoMoreOutput is a dummy error, whose message is ignored, and which 56 // causes the app to quit immediately and successfully 57 var errNoMoreOutput = errors.New(`no more output`) 58 59 // errNoThumb is a generic error to handle lack of thumbnails, in case no 60 // picture-metadata-starters are found at all 61 var errNoThumb = errors.New(`no thumbnail data found`) 62 63 const errorStyle = "\x1b[31m" 64 65 func main() { 66 if len(os.Args) > 1 { 67 switch os.Args[1] { 68 case `-h`, `--h`, `-help`, `--help`: 69 os.Stderr.WriteString(info[1:]) 70 return 71 } 72 } 73 74 if len(os.Args) > 2 { 75 os.Stderr.WriteString(errorStyle) 76 os.Stderr.WriteString(`can only handle 1 file`) 77 os.Stderr.WriteString("\x1b[0m\n") 78 os.Exit(1) 79 } 80 81 name := `-` 82 if len(os.Args) > 1 { 83 name = os.Args[1] 84 } 85 86 if err := run(os.Stdout, name); isActualError(err) { 87 os.Stderr.WriteString(errorStyle) 88 os.Stderr.WriteString(err.Error()) 89 os.Stderr.WriteString("\x1b[0m\n") 90 os.Exit(1) 91 } 92 } 93 94 func run(w io.Writer, name string) error { 95 if name == `-` { 96 return id3pic(w, bufio.NewReader(os.Stdin)) 97 } 98 99 f, err := os.Open(name) 100 if err != nil { 101 return errors.New(`can't read from file named "` + name + `"`) 102 } 103 defer f.Close() 104 105 return id3pic(w, bufio.NewReader(f)) 106 } 107 108 // isActualError is to figure out whether not to ignore an error, and thus 109 // show it as an error message 110 func isActualError(err error) bool { 111 return err != nil && err != io.EOF && err != errNoMoreOutput 112 } 113 114 func match(r *bufio.Reader, what []byte) bool { 115 for _, v := range what { 116 b, err := r.ReadByte() 117 if b != v || err != nil { 118 return false 119 } 120 } 121 return true 122 } 123 124 func id3pic(w io.Writer, r *bufio.Reader) error { 125 for { 126 b, err := r.ReadByte() 127 if err == io.EOF { 128 return errNoThumb 129 } 130 if err != nil { 131 return err 132 } 133 134 // handle APIC-type chunks 135 if b == 'A' { 136 if match(r, []byte{'P', 'I', 'C'}) { 137 // section-size seems stored as 4 big-endian bytes 138 var size uint32 139 err := binary.Read(r, binary.BigEndian, &size) 140 if err != nil { 141 return err 142 } 143 144 // a, err1 := r.ReadByte() 145 // b, err2 := r.ReadByte() 146 // c, err3 := r.ReadByte() 147 // d, err4 := r.ReadByte() 148 // if err1 != nil || err2 != nil || err3 != nil || err4 != nil { 149 // continue 150 // } 151 152 // var size int64 = int(a)<<24 + int(b)<<16 + int(c)<<8 + int(d) 153 154 n, err := skipThumbnailTypeAPIC(r) 155 if err != nil { 156 continue 157 } 158 159 _, err = io.Copy(w, io.LimitReader(r, int64(int(size)-n))) 160 return err 161 } 162 } 163 164 // handle PIC-type chunks 165 if b == 'P' { 166 if match(r, []byte{'I', 'C'}) { 167 // http://www.unixgods.org/Ruby/ID3/docs/id3v2-00.html#PIC 168 169 // thumbnail-payload-size seems stored as 3 big-endian bytes 170 a, err1 := r.ReadByte() 171 b, err2 := r.ReadByte() 172 c, err3 := r.ReadByte() 173 if err1 != nil || err2 != nil || err3 != nil { 174 continue 175 } 176 177 // skip the text encoding 178 n, err := r.Discard(5) 179 if n != 5 || err != nil { 180 continue 181 } 182 183 // skip a null-delimited string 184 _, err = r.ReadSlice(0) 185 if err != nil { 186 continue 187 } 188 189 size := int(a)<<16 + int(b)<<8 + int(c) 190 _, err = io.Copy(w, io.LimitReader(r, int64(size))) 191 return err 192 } 193 } 194 } 195 } 196 197 func skipThumbnailTypeAPIC(r *bufio.Reader) (int, error) { 198 n := 0 199 _, err := r.Discard(1) 200 if err != nil { 201 return -1, errors.New(`failed to sync APIC thumbnail text-encoding`) 202 } 203 n++ 204 205 junk, err := r.ReadSlice('/') 206 if err != nil { 207 return -1, errors.New(`failed to sync to APIC thumbnail MIME-type`) 208 } 209 n += len(junk) 210 211 // skip a null-delimited string 212 junk, err = r.ReadSlice(0) 213 if err != nil { 214 return -1, errors.New(`failed to sync to APIC thumbnail MIME-type`) 215 } 216 n += len(junk) 217 218 _, err = r.Discard(1) 219 if err != nil { 220 return -1, errors.New(`failed to sync APIC thumbnail picture type`) 221 } 222 n++ 223 224 // skip a null-delimited string 225 junk, err = r.ReadSlice(0) 226 if err != nil { 227 return -1, errors.New(`failed to sync to thumbnail comment`) 228 } 229 n += len(junk) 230 231 return n, nil 232 }