github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/cmd/nup/debug/command.go (about) 1 // Copyright 2022 Daniel Erat. 2 // All rights reserved. 3 4 package debug 5 6 import ( 7 "context" 8 "flag" 9 "fmt" 10 "math" 11 "os" 12 "strings" 13 "time" 14 15 "github.com/derat/nup/cmd/nup/client" 16 "github.com/google/subcommands" 17 ) 18 19 type Command struct { 20 Cfg *client.Config 21 22 id3 bool // print all ID3v2 text frames 23 mpeg bool // read MPEG frames and print size/duration 24 } 25 26 func (*Command) Name() string { return "debug" } 27 func (*Command) Synopsis() string { return "print information about a song file" } 28 func (*Command) Usage() string { 29 return `debug <flags> <path>...: 30 Print information about one or more song files. 31 32 ` 33 } 34 35 func (cmd *Command) SetFlags(f *flag.FlagSet) { 36 f.BoolVar(&cmd.id3, "id3", false, "Print all ID3v2 text frames") 37 f.BoolVar(&cmd.mpeg, "mpeg", false, "Read MPEG frames and print size/duration info") 38 } 39 40 func (cmd *Command) Execute(ctx context.Context, fs *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { 41 if fs.NArg() < 1 { 42 fmt.Fprintln(os.Stderr, cmd.Usage()) 43 return subcommands.ExitUsageError 44 } 45 if !cmd.id3 && !cmd.mpeg { 46 fmt.Fprintln(os.Stderr, "No action requested via flags") 47 return subcommands.ExitUsageError 48 } 49 50 var failed bool 51 for i, p := range fs.Args() { 52 if len(fs.Args()) > 1 { 53 if i > 0 { 54 fmt.Println() 55 } 56 fmt.Println(p) 57 } 58 if cmd.id3 { 59 if err := cmd.doID3(p); err != nil { 60 fmt.Fprintln(os.Stderr, "Failed reading ID3 tag:", err) 61 failed = true 62 } 63 } 64 if cmd.mpeg { 65 if err := cmd.doMPEG(p); err != nil { 66 fmt.Fprintln(os.Stderr, "Failed reading MPEG frames:", err) 67 failed = true 68 } 69 } 70 } 71 if failed { 72 return subcommands.ExitFailure 73 } 74 return subcommands.ExitSuccess 75 } 76 77 func (cmd *Command) doID3(p string) error { 78 frames, err := readID3Frames(p) 79 if err != nil { 80 return err 81 } 82 for _, frame := range frames { 83 var val string 84 if len(frame.fields) == 0 { 85 val = fmt.Sprintf("[%d bytes]", frame.size) 86 } else { 87 quoted := make([]string, len(frame.fields)) 88 for i, s := range frame.fields { 89 quoted[i] = fmt.Sprintf("%q", s) 90 } 91 val = strings.Join(quoted, " ") 92 } 93 fmt.Println(frame.id + " " + val) 94 } 95 return nil 96 } 97 98 func (cmd *Command) doMPEG(p string) error { 99 info, err := getMPEGInfo(p) 100 if err != nil { 101 return err 102 } 103 104 fmt.Printf("%d bytes: %d header, %d data, %d footer (%v)\n", 105 info.size, info.header, info.size-info.header-info.footer, info.footer, info.sha1) 106 for _, s := range info.skipped { 107 fmt.Printf(" skipped %d-%d (%d) %v\n", s.offset, s.offset+s.size-1, s.size, s.err) 108 } 109 110 var actualBitrate string 111 if info.vbr { 112 actualBitrate = fmt.Sprintf("%0.1f kb/s VBR", info.avgKbitRate) 113 } else { 114 actualBitrate = fmt.Sprintf("%0.0f kb/s CBR", math.Round(info.avgKbitRate)) 115 } 116 117 format := func(d time.Duration) string { 118 return fmt.Sprintf("%d:%06.3f", int(d.Minutes()), (d % time.Minute).Seconds()) 119 } 120 var enc string 121 if info.xingEnc != "" { 122 enc = ", " + info.xingEnc 123 } 124 fmt.Printf("Xing: %s (%d frames, %d data%s)\n", 125 format(info.xingDur), info.xingFrames, info.xingBytes, enc) 126 fmt.Printf("Actual: %s (%d frames, %d data, %s)\n", 127 format(info.actualDur), info.actualFrames, info.actualBytes, actualBitrate) 128 if info.emptyFrame >= 0 { 129 fmt.Printf("Audio: %s (%d frames, then empty starting at offset %d)\n", 130 format(info.emptyTime), info.emptyFrame, info.emptyOffset) 131 } 132 return nil 133 }