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 // match the ID3 mark 129 for { 130 b, err := r.ReadByte() 131 if err == io.EOF { 132 return errNoThumb 133 } 134 if err != nil { 135 return err 136 } 137 138 if b == 'I' && match(r, []byte{'D', '3'}) { 139 break 140 } 141 } 142 143 for { 144 b, err := r.ReadByte() 145 if err == io.EOF { 146 return errNoThumb 147 } 148 if err != nil { 149 return err 150 } 151 152 // handle APIC-type chunks 153 if b == 'A' && match(r, []byte{'P', 'I', 'C'}) { 154 return handleAPIC(w, r) 155 } 156 } 157 } 158 159 func handleAPIC(w io.Writer, r *bufio.Reader) error { 160 // section-size seems stored as 4 big-endian bytes 161 var size uint32 162 err := binary.Read(r, binary.BigEndian, &size) 163 if err != nil { 164 return err 165 } 166 167 n, err := skipThumbnailTypeAPIC(r) 168 if err != nil { 169 return err 170 } 171 172 _, err = io.Copy(w, io.LimitReader(r, int64(int(size)-n))) 173 return err 174 } 175 176 func skipThumbnailTypeAPIC(r *bufio.Reader) (skipped int, err error) { 177 m, err := r.Discard(2) 178 if err != nil || m != 2 { 179 return -1, errors.New(`failed to sync APIC flags`) 180 } 181 skipped += m 182 183 m, err = r.Discard(1) 184 if err != nil || m != 1 { 185 return -1, errors.New(`failed to sync APIC text-encoding`) 186 } 187 skipped += m 188 189 junk, err := r.ReadSlice(0) 190 if err != nil { 191 return -1, errors.New(`failed to sync to APIC thumbnail MIME-type`) 192 } 193 skipped += len(junk) 194 195 m, err = r.Discard(1) 196 if err != nil || m != 1 { 197 return -1, errors.New(`failed to sync APIC picture type`) 198 } 199 skipped += m 200 201 junk, err = r.ReadSlice(0) 202 if err != nil { 203 return -1, errors.New(`failed to sync to APIC thumbnail description`) 204 } 205 skipped += len(junk) 206 207 return skipped, nil 208 }