command line tool for adding ID3 chapters to an MP3 file
at master 139 lines 3.9 kB view raw
1// 2// mp3chap 3// Copyright (c) 2017 joshua stein <jcs@jcs.org> 4// 5// Redistribution and use in source and binary forms, with or without 6// modification, are permitted provided that the following conditions 7// are met: 8// 9// 1. Redistributions of source code must retain the above copyright 10// notice, this list of conditions and the following disclaimer. 11// 2. Redistributions in binary form must reproduce the above copyright 12// notice, this list of conditions and the following disclaimer in the 13// documentation and/or other materials provided with the distribution. 14// 3. The name of the author may not be used to endorse or promote products 15// derived from this software without specific prior written permission. 16// 17// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 18// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20// IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 21// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 22// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27// 28 29package main 30 31import ( 32 "fmt" 33 id3 "github.com/jcs/id3-go" 34 id3v2 "github.com/jcs/id3-go/v2" 35 "log" 36 "os" 37 "strconv" 38) 39 40type Chapter struct { 41 element string 42 startSecs uint32 43 endSecs uint32 44 title string 45} 46 47func usage() { 48 fmt.Printf("%s: <mp3 file> [<start seconds> <chapter label> ...]\n", os.Args[0]) 49 os.Exit(1) 50} 51 52func main() { 53 if len(os.Args) < 4 { 54 usage() 55 } 56 57 fn := os.Args[1] 58 mp3, err := id3.Open(fn) 59 if err != nil { 60 log.Fatal("can't open %s: %s", fn, err) 61 } 62 63 chaps := make([]Chapter, 0) 64 tocchaps := make([]string, 0) 65 finalEnd := uint64(0) 66 67 x := 2 68 for ; x < len(os.Args)-1; x += 2 { 69 if x+1 > len(os.Args)-2+1 { 70 usage() 71 } 72 73 startSecs, err := strconv.ParseUint(os.Args[x], 10, 32) 74 if err != nil { 75 log.Fatal("failed parsing seconds %#v: %v", os.Args[x], err) 76 } 77 78 element := fmt.Sprintf("chp%d", len(chaps)) 79 tocchaps = append(tocchaps, element) 80 81 chap := Chapter{element, uint32(startSecs * 1000), 0, os.Args[x+1]} 82 chaps = append(chaps, chap) 83 } 84 85 // if there is a final odd arg, use it as the final chapter end 86 if x < len(os.Args) { 87 finalEnd, err = strconv.ParseUint(os.Args[x], 10, 32) 88 if err != nil { 89 log.Fatal("failed parsing final seconds %#v: %v", os.Args[x], err) 90 } else { 91 finalEnd = uint64(finalEnd * 1000) 92 } 93 } 94 95 // each chapter ends where the next one starts 96 for x := range chaps { 97 if x < len(chaps)-1 { 98 chaps[x].endSecs = chaps[x+1].startSecs 99 } 100 } 101 102 if finalEnd == 0 { 103 // and the last one ends when the file does 104 tlenf := mp3.Frame("TLEN") 105 if tlenf == nil { 106 log.Fatal("can't find TLEN frame, don't know total duration") 107 } 108 tlenft, ok := tlenf.(*id3v2.TextFrame) 109 if !ok { 110 log.Fatal("can't convert TLEN to TextFrame") 111 } 112 tlen, err := strconv.ParseUint(tlenft.Text(), 10, 32) 113 if err == nil { 114 log.Fatal("can't parse TLEN value %#v\n", tlenft.Text()) 115 } 116 117 chaps[len(chaps)-1].endSecs = uint32(tlen) 118 } else { 119 chaps[len(chaps)-1].endSecs = uint32(finalEnd) 120 } 121 122 // ready to modify the file, clear out what's there 123 mp3.DeleteFrames("CTOC") 124 mp3.DeleteFrames("CHAP") 125 126 // build a new TOC referencing each chapter 127 ctocft := id3v2.V23FrameTypeMap["CTOC"] 128 toc := id3v2.NewTOCFrame(ctocft, "toc", true, true, tocchaps) 129 mp3.AddFrames(toc) 130 131 // add each chapter 132 chapft := id3v2.V23FrameTypeMap["CHAP"] 133 for _, c := range chaps { 134 ch := id3v2.NewChapterFrame(chapft, c.element, c.startSecs, c.endSecs, 0, 0, true, c.title, "", "") 135 mp3.AddFrames(ch) 136 } 137 138 mp3.Close() 139}