@@ -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+
185292func splitArgPaths (argPaths []string ) (args []string , paths []string ) {
186293 if len (argPaths ) == 0 {
187294 return nil , nil
0 commit comments