Skip to content

Commit f50eb9f

Browse files
committed
feat(metadata): add image properties, image-read, image-write, image-clear
1 parent d454f6d commit f50eb9f

File tree

2 files changed

+119
-0
lines changed

2 files changed

+119
-0
lines changed

cmd/metadata/main.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ func init() {
2828
fmt.Fprintf(flag.Output(), " $ %s [<options>] read <tag>... -- <path>...\n", flag.Name())
2929
fmt.Fprintf(flag.Output(), " $ %s [<options>] write ( <tag> <value>... , )... -- <path>...\n", flag.Name())
3030
fmt.Fprintf(flag.Output(), " $ %s [<options>] clear <tag>... -- <path>...\n", flag.Name())
31+
fmt.Fprintf(flag.Output(), " $ %s [<options>] image-read [-index <n>] -- <path>\n", flag.Name())
32+
fmt.Fprintf(flag.Output(), " $ %s [<options>] image-write [-index <n>] [-type <type>] [-desc <desc>] <image-path> -- <path>...\n", flag.Name())
33+
fmt.Fprintf(flag.Output(), " $ %s [<options>] image-clear -- <path>...\n", flag.Name())
3134
fmt.Fprintf(flag.Output(), "\n")
3235
fmt.Fprintf(flag.Output(), " # <tag> is an audio metadata tag key\n")
3336
fmt.Fprintf(flag.Output(), " # <value> is an audio metadata tag value\n")
3437
fmt.Fprintf(flag.Output(), " # <path> is path(s) to audio files, dir(s) to find audio files in, or \"-\" for list audio file paths from stdin\n")
38+
fmt.Fprintf(flag.Output(), " # <image-path> is path to an image file to embed\n")
3539
fmt.Fprintf(flag.Output(), "\n")
3640
fmt.Fprintf(flag.Output(), "Options:\n")
3741
flag.PrintDefaults()
@@ -46,12 +50,19 @@ func init() {
4650
fmt.Fprintf(flag.Output(), " $ %s write artist \"Sensient\" , genres \"psy\" \"minimal\" \"techno\" -- dir/\n", flag.Name())
4751
fmt.Fprintf(flag.Output(), " $ %s clear -- a.flac\n", flag.Name())
4852
fmt.Fprintf(flag.Output(), " $ %s clear lyrics artist_credit -- *.flac\n", flag.Name())
53+
fmt.Fprintf(flag.Output(), " $ %s image-read -- a.flac > cover.jpg\n", flag.Name())
54+
fmt.Fprintf(flag.Output(), " $ %s image-read -index 1 -- a.flac > back.jpg\n", flag.Name())
55+
fmt.Fprintf(flag.Output(), " $ %s image-write cover.jpg -- a.flac b.flac\n", flag.Name())
56+
fmt.Fprintf(flag.Output(), " $ %s image-write -index 2 -type \"Back Cover\" -desc \"Album back\" back.jpg -- a.flac\n", flag.Name())
57+
fmt.Fprintf(flag.Output(), " $ %s image-clear -- a.flac b.flac\n", flag.Name())
4958
fmt.Fprintf(flag.Output(), " $ find x/ -type f | %s write artist \"Sensient\" , album \"Blue Neevus\" -\n", flag.Name())
5059
fmt.Fprintf(flag.Output(), " $ find y/ -type f | %s read artist title -\n", flag.Name())
5160
fmt.Fprintf(flag.Output(), " $ find y/ -type f -name \"*extended*\" | %s read -properties length -\n", flag.Name())
5261
fmt.Fprintf(flag.Output(), "\n")
5362
fmt.Fprintf(flag.Output(), "See also:\n")
5463
fmt.Fprintf(flag.Output(), " $ %s read -h\n", flag.Name())
64+
fmt.Fprintf(flag.Output(), " $ %s image-read -h\n", flag.Name())
65+
fmt.Fprintf(flag.Output(), " $ %s image-write -h\n", flag.Name())
5566
}
5667
}
5768

@@ -93,6 +104,54 @@ func main() {
93104
slog.Error("process clear", "err", err)
94105
return
95106
}
107+
case "image-read":
108+
flag := flag.NewFlagSet(command, flag.ExitOnError)
109+
var (
110+
index = flag.Int("index", 0, "Image index to read (0 = first)")
111+
)
112+
flag.Parse(args)
113+
114+
_, paths := splitArgPaths(flag.Args())
115+
if len(paths) != 1 {
116+
slog.Error("image-read requires exactly one audio file path")
117+
return
118+
}
119+
path := paths[0]
120+
121+
out := bufio.NewWriter(os.Stdout)
122+
defer out.Flush()
123+
124+
if err := cmdImageRead(out, path, *index); err != nil {
125+
slog.Error("process image-read", "err", err)
126+
return
127+
}
128+
case "image-write":
129+
flag := flag.NewFlagSet(command, flag.ExitOnError)
130+
var (
131+
index = flag.Int("index", 0, "Image index to write to (0 indexed)")
132+
typ = flag.String("type", "Front Cover", "Picture type")
133+
mime = flag.String("mime-type", "", "Image MIME type")
134+
desc = flag.String("desc", "", "Image description")
135+
)
136+
flag.Parse(args)
137+
138+
args, paths := splitArgPaths(flag.Args())
139+
if len(args) != 1 {
140+
slog.Error("image-write requires exactly one image file path")
141+
return
142+
}
143+
imagePath := args[0]
144+
145+
if err := iterFiles(paths, func(p string) error { return cmdImageWrite(p, imagePath, *index, *typ, *desc, *mime) }); err != nil {
146+
slog.Error("process image-write", "err", err)
147+
return
148+
}
149+
case "image-clear":
150+
_, paths := splitArgPaths(args)
151+
if err := iterFiles(paths, func(p string) error { return cmdImageClear(p) }); err != nil {
152+
slog.Error("process image-clear", "err", err)
153+
return
154+
}
96155
default:
97156
slog.Error("unknown command", "command", command)
98157
return
@@ -151,6 +210,21 @@ func cmdRead(to io.Writer, path string, withProperties bool, keys []string) erro
151210
fmt.Fprintf(to, "%s\t%s\t%d\n", path, k, properties.Channels)
152211
}
153212

213+
for i, image := range properties.Images {
214+
if k := "image_index"; wantProperty(k) {
215+
fmt.Fprintf(to, "%s\t%s\t%d\n", path, k, i)
216+
}
217+
if k := "image_type"; wantProperty(k) {
218+
fmt.Fprintf(to, "%s\t%s\t%s\n", path, k, image.Type)
219+
}
220+
if k := "image_description"; wantProperty(k) {
221+
fmt.Fprintf(to, "%s\t%s\t%s\n", path, k, image.Description)
222+
}
223+
if k := "image_mime_type"; wantProperty(k) {
224+
fmt.Fprintf(to, "%s\t%s\t%s\n", path, k, image.MimeType)
225+
}
226+
}
227+
154228
return nil
155229
}
156230

@@ -182,6 +256,39 @@ func cmdClear(path string, keys []string) error {
182256
return nil
183257
}
184258

259+
func cmdImageRead(to io.Writer, path string, index int) error {
260+
data, err := tags.ReadImageOptions(path, index)
261+
if err != nil {
262+
return fmt.Errorf("read image: %w", err)
263+
}
264+
if len(data) == 0 {
265+
return fmt.Errorf("no image found at index %d in %s", index, path)
266+
}
267+
if _, err := to.Write(data); err != nil {
268+
return fmt.Errorf("write image: %w", err)
269+
}
270+
return nil
271+
}
272+
273+
func cmdImageWrite(audioPath string, imagePath string, index int, imageType, description, imageMIMEType string) error {
274+
data, err := os.ReadFile(imagePath) //nolint:gosec // path is from user's argument
275+
if err != nil {
276+
return fmt.Errorf("read image file: %w", err)
277+
}
278+
279+
if err := tags.WriteImageOptions(audioPath, data, index, imageType, description, imageMIMEType); err != nil {
280+
return fmt.Errorf("write image: %w", err)
281+
}
282+
return nil
283+
}
284+
285+
func cmdImageClear(audioPath string) error {
286+
if err := tags.WriteImage(audioPath, nil); err != nil {
287+
return fmt.Errorf("clear images: %w", err)
288+
}
289+
return nil
290+
}
291+
185292
func splitArgPaths(argPaths []string) (args []string, paths []string) {
186293
if len(argPaths) == 0 {
187294
return nil, nil

tags/tags.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ func ReadImage(path string) ([]byte, error) {
4040
return taglib.ReadImage(path)
4141
}
4242

43+
func ReadImageOptions(path string, index int) ([]byte, error) {
44+
return taglib.ReadImageOptions(path, index)
45+
}
46+
47+
func WriteImage(path string, image []byte) error {
48+
return taglib.WriteImage(path, image)
49+
}
50+
51+
func WriteImageOptions(path string, image []byte, index int, imageType, description, mimeType string) error {
52+
return taglib.WriteImageOptions(path, image, index, imageType, description, mimeType)
53+
}
54+
4355
type Properties = taglib.Properties
4456

4557
func ReadProperties(path string) (Properties, error) {

0 commit comments

Comments
 (0)