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 // errInvalidPIC is a generic error for invalid PIC-format pics 64 var errInvalidPIC = errors.New(`invalid PIC-format embedded thumbnail`) 65 66 func main() { 67 if len(os.Args) > 1 { 68 switch os.Args[1] { 69 case `-h`, `--h`, `-help`, `--help`: 70 os.Stderr.WriteString(info[1:]) 71 return 72 } 73 } 74 75 if len(os.Args) > 2 { 76 showError(`can only handle 1 file`) 77 os.Exit(1) 78 } 79 80 name := `-` 81 if len(os.Args) > 1 { 82 name = os.Args[1] 83 } 84 85 if err := run(os.Stdout, name); isActualError(err) { 86 showError(err.Error()) 87 os.Exit(1) 88 } 89 } 90 91 func showError(msg string) { 92 os.Stderr.WriteString("\x1b[31m") 93 os.Stderr.WriteString(msg) 94 os.Stderr.WriteString("\x1b[0m\n") 95 } 96 97 func run(w io.Writer, name string) error { 98 if name == `-` { 99 return id3pic(w, bufio.NewReader(os.Stdin)) 100 } 101 102 f, err := os.Open(name) 103 if err != nil { 104 return errors.New(`can't read from file named "` + name + `"`) 105 } 106 defer f.Close() 107 108 return id3pic(w, bufio.NewReader(f)) 109 } 110 111 // isActualError is to figure out whether not to ignore an error, and thus 112 // show it as an error message 113 func isActualError(err error) bool { 114 return err != nil && err != io.EOF && err != errNoMoreOutput 115 } 116 117 func match(r *bufio.Reader, what []byte) bool { 118 for _, v := range what { 119 b, err := r.ReadByte() 120 if b != v || err != nil { 121 return false 122 } 123 } 124 return true 125 } 126 127 func id3pic(w io.Writer, r *bufio.Reader) error { 128 for { 129 b, err := r.ReadByte() 130 if err == io.EOF { 131 return errNoThumb 132 } 133 if err != nil { 134 return err 135 } 136 137 // handle APIC-type chunks 138 if b == 'A' && match(r, []byte{'P', 'I', 'C'}) { 139 return handleAPIC(w, r) 140 } 141 142 // handle PIC-type chunks 143 if b == 'P' && match(r, []byte{'I', 'C'}) { 144 return handlePIC(w, r) 145 } 146 } 147 } 148 149 func handleAPIC(w io.Writer, r *bufio.Reader) error { 150 // section-size seems stored as 4 big-endian bytes 151 var size uint32 152 err := binary.Read(r, binary.BigEndian, &size) 153 if err != nil { 154 return err 155 } 156 157 n, err := skipThumbnailTypeAPIC(r) 158 if err != nil { 159 return err 160 } 161 162 _, err = io.Copy(w, io.LimitReader(r, int64(int(size)-n))) 163 return err 164 } 165 166 func skipThumbnailTypeAPIC(r *bufio.Reader) (skipped int, err error) { 167 // https://id3.org/id3v2.3.0 168 169 m, err := r.Discard(2) 170 if err != nil || m != 2 { 171 return -1, errors.New(`failed to sync APIC flags`) 172 } 173 skipped += m 174 175 m, err = r.Discard(1) 176 if err != nil || m != 1 { 177 return -1, errors.New(`failed to sync APIC text-encoding`) 178 } 179 skipped += m 180 181 junk, err := r.ReadSlice(0) 182 if err != nil { 183 return -1, errors.New(`failed to sync to APIC thumbnail MIME-type`) 184 } 185 skipped += len(junk) 186 187 m, err = r.Discard(1) 188 if err != nil || m != 1 { 189 return -1, errors.New(`failed to sync APIC picture type`) 190 } 191 skipped += m 192 193 junk, err = r.ReadSlice(0) 194 if err != nil { 195 return -1, errors.New(`failed to sync to APIC thumbnail description`) 196 } 197 skipped += len(junk) 198 199 return skipped, nil 200 } 201 202 func handlePIC(w io.Writer, r *bufio.Reader) error { 203 // http://www.unixgods.org/Ruby/ID3/docs/id3v2-00.html#PIC 204 205 // thumbnail-payload-size seems stored as 3 big-endian bytes 206 a, err1 := r.ReadByte() 207 b, err2 := r.ReadByte() 208 c, err3 := r.ReadByte() 209 if err1 != nil || err2 != nil || err3 != nil { 210 return errors.New(`failed to read PIC thumbnail size`) 211 } 212 213 // skip the text encoding 214 n, err := r.Discard(5) 215 if n != 5 || err != nil { 216 return errInvalidPIC 217 } 218 219 // skip a null-delimited string 220 _, err = r.ReadSlice(0) 221 if err != nil { 222 return errInvalidPIC 223 } 224 225 size := int64(a)<<16 + int64(b)<<8 + int64(c) 226 _, err = io.Copy(w, io.LimitReader(r, size)) 227 return err 228 }