command line tool for adding ID3 chapters to an MP3 file
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}