1515package cmd
1616
1717import (
18+ "encoding/json"
19+ "fmt"
1820 "log"
1921 "os"
2022 "os/exec"
23+ "path"
24+ "path/filepath"
25+ "strings"
26+ "sync"
2127
28+ "github.com/kennygrant/sanitize"
2229 "github.com/spf13/cobra"
2330)
2431
25- // lastUsedCmd represents the lastUsed command
2632var chapterListCmd = & cobra.Command {
27- Use : "chapterlist" ,
33+ Use : "chapterlist <videofile.mp4> " ,
2834 Short : "List chapters in a video container." ,
2935 Long : `Use ffprobe to discover all chapter metadata in a video file container.` ,
3036 Run : chapterList ,
3137 Args : cobra .ExactArgs (1 ),
3238}
3339
34- func chapterList (cmd * cobra.Command , args []string ) {
40+ var chapterSplitCmd = & cobra.Command {
41+ Use : "chaptersplit <videofile.mp4>" ,
42+ Short : "Split video file into separate files per chapter." ,
43+ Long : `Use ffmpeg to copy each chapter from a video file into it's own file.` ,
44+ Run : chapterSplit ,
45+ Args : cobra .ExactArgs (1 ),
46+ }
47+
48+ func chapterSplit (cmd * cobra.Command , args []string ) {
3549
3650 _ , err := exec .LookPath ("ffprobe" )
3751
@@ -45,39 +59,145 @@ func chapterList(cmd *cobra.Command, args []string) {
4559 log .Fatal ("Could not find ffmpeg. Please install ffmpeg." )
4660 }
4761
48- _ , err = exec .LookPath ("jq" )
62+ target := args [0 ]
63+ _ , err = os .Stat (target )
4964
5065 if err != nil {
51- log .Fatal ("Could not find jq. Please install jq." )
66+ log .Fatal ("Could not access video container " , target )
5267 }
5368
54- target := args [0 ]
55- _ , err = os .Stat (target )
69+ data , err := getChapters (target )
70+ base := strings .Trim (path .Base (target ), path .Ext (target ))
71+ targetdir := fmt .Sprintf ("split_%s" , base )
72+ err = os .MkdirAll (targetdir , 0777 )
5673
5774 if err != nil {
58- log .Fatal ("Could not access video container " , target )
75+ log .Fatal (err )
76+ }
77+
78+ var wg sync.WaitGroup
79+
80+ for _ , c := range data .Chapters {
81+ wg .Add (1 )
82+ go copyChapter (& wg , c , target , targetdir )
5983 }
6084
85+ wg .Wait ()
86+ }
87+
88+ func copyChapter (wg * sync.WaitGroup , c chapter , sourcefile , targetdir string ) error {
89+ defer wg .Done ()
90+
91+ title := strings .Trim (c .Tags .Title , " \n \r " )
92+ safetitle := sanitize .Name (title )
93+ prefix := fmt .Sprintf ("%03d_" , c .Id )
94+ outfile := filepath .Join (targetdir , prefix + safetitle + path .Ext (sourcefile ))
95+
96+ cmd := exec .Command ("ffmpeg" ,
97+ "-loglevel" , "error" ,
98+ "-i" , sourcefile ,
99+ "-c" , "copy" ,
100+ "-map" , "0" ,
101+ "-ss" , c .StartTime ,
102+ "-to" , c .EndTime ,
103+ outfile )
104+
105+ output , err := cmd .CombinedOutput ()
106+
107+ if err != nil {
108+ fmt .Printf ("%s: %s\n " , outfile , output )
109+ }
110+
111+ return err
112+ }
113+
114+ // sample json output
115+ // {
116+ // "chapters": [
117+ // {
118+ // "id": 0,
119+ // "time_base": "1/1000",
120+ // "start": 0,
121+ // "start_time": "0.000000",
122+ // "end": 6006,
123+ // "end_time": "6.006000",
124+ // "tags": {
125+ // "title": "Title Page\r"
126+ // }
127+ // }
128+ // ]
129+ // }
130+
131+ type tags struct {
132+ Title string `json:"title"`
133+ }
134+
135+ type chapter struct {
136+ StartTime string `json:"start_time"`
137+ EndTime string `json:"end_time"`
138+ Id int `json:"id"`
139+ Tags tags `json:"tags"`
140+ }
141+
142+ type ffmprobeResponse struct {
143+ Chapters []chapter `json:"chapters"`
144+ }
145+
146+ func getChapters (target string ) (ffmprobeResponse , error ) {
61147 command := exec .Command ("ffprobe" ,
62148 "-print_format" , "json" ,
63149 "-loglevel" , "error" ,
64150 "-show_chapters" ,
65151 "-i" , target )
66152
67- output , _ := command .Output ()
68- _ , _ = os .Stdout .Write (output )
153+ output , err := command .Output ()
154+ response := ffmprobeResponse {}
155+
156+ if err != nil {
157+ return response , err
158+ }
159+
160+ err = json .Unmarshal (output , & response )
161+
162+ return response , err
69163}
70164
71- func init () {
72- rootCmd .AddCommand (chapterListCmd )
165+ func chapterList (cmd * cobra.Command , args []string ) {
73166
74- // Here you will define your flags and configuration settings.
167+ _ , err := exec . LookPath ( "ffprobe" )
75168
76- // Cobra supports Persistent Flags which will work for this command
77- // and all subcommands, e.g.:
78- // dryrunCmd.PersistentFlags().String("foo", "", "A help for foo")
169+ if err != nil {
170+ log .Fatal ("Could not find ffprobe. Please install ffmpeg and ffprobe." )
171+ }
172+
173+ _ , err = exec .LookPath ("ffmpeg" )
79174
80- // Cobra supports local flags which will only run when this command
81- // is called directly, e.g.:
175+ if err != nil {
176+ log .Fatal ("Could not find ffmpeg. Please install ffmpeg." )
177+ }
82178
179+ target := args [0 ]
180+ _ , err = os .Stat (target )
181+
182+ if err != nil {
183+ log .Fatal ("Could not access video container " , target )
184+ }
185+
186+ data , err := getChapters (target )
187+
188+ if err != nil {
189+ log .Fatal ("Problem getting chapter data " , err )
190+ }
191+
192+ formattedJSON , err := json .MarshalIndent (data , "" , " " )
193+ if err != nil {
194+ log .Fatal (err )
195+ }
196+
197+ _ , _ = os .Stdout .Write (formattedJSON )
198+ }
199+
200+ func init () {
201+ rootCmd .AddCommand (chapterListCmd )
202+ rootCmd .AddCommand (chapterSplitCmd )
83203}
0 commit comments