func TestFFmpegSplitTimes(t *testing.T) { // We need to make up last track's duration: 3 minutes. totaltime := float64(17*60 + 4 + 3*60) want := []struct { track int start string duration string }{ {track: 0, start: "00:00:00.000", duration: "00:06:40.360"}, {track: 1, start: "00:06:40.360", duration: "00:04:13.640"}, {track: 3, start: "00:17:04.000", duration: "00:03:00.000"}, {track: 4, start: "", duration: ""}, {track: 8, start: "", duration: ""}, } buf, err := ioutil.ReadFile(sampleCuesheet) if err != nil { panic(err) } sheet, err := cuesheet.New(buf) if err != nil { panic(err) } for _, v := range want { start, duration := ffmpegSplitTimes(sheet, "Faithless - Live in Berlin (CD1).mp3", v.track, totaltime) if start != v.start || duration != v.duration { t.Errorf("Got {start: %v, duration: %v}, want {start: %v, duration: %v}", start, duration, v.start, v.duration) } } }
// prepareInput sets the details of 'info' as returned by ffprobe. // As a special case, if 'info' is 'fr.input', then 'fr.Format' and // 'fr.Streams': those values will be needed later in the pipeline. func prepareInput(fr *FileRecord, info *inputInfo) error { cmd := exec.Command("ffprobe", "-v", "error", "-print_format", "json", "-show_streams", "-show_format", info.path) var stderr bytes.Buffer cmd.Stderr = &stderr out, err := cmd.Output() if err != nil { fr.error.Print("ffprobe: ", stderr.String()) return err } err = json.Unmarshal(out, info) if err != nil { fr.error.Print(err) return err } // probed need not be initialized since we only use it to temporarily store // the 'Format' and 'Streams' structures returned by 'ffprobe'. var probed FileRecord err = json.Unmarshal(out, &probed) if err != nil { fr.error.Print(err) return err } if info == &fr.input { fr.Format = probed.Format fr.Streams = probed.Streams } // Get modification time. fi, err := os.Lstat(info.path) if err != nil { fr.error.Print(err) return err } info.modTime.sec = fi.ModTime().Unix() info.modTime.nsec = fi.ModTime().Nanosecond() // Index of the first audio stream. info.audioIndex = -1 for k, v := range probed.Streams { if v.CodecType == "audio" { info.audioIndex = k break } } if info.audioIndex == -1 { fr.warning.Print("Non-audio file:", info.path) return errNonAudio } info.tags = make(map[string]string) info.filetags = make(map[string]string) // Precedence: cuesheet > stream tags > format tags. for k, v := range probed.Format.Tags { info.filetags[strings.ToLower(k)] = v } for k, v := range probed.Streams[info.audioIndex].Tags { key := strings.ToLower(k) _, ok := info.filetags[key] if !ok || info.filetags[key] == "" { info.filetags[key] = v } } var ErrCuesheet error info.cuesheet, ErrCuesheet = cuesheet.New([]byte(info.filetags["cuesheet"])) if err != nil { // If no cuesheet was found in the tags, we check for external ones. pathNoext := StripExt(info.path) // Instead of checking the extension of files in current folder, we check // if a file with the 'cue' extension exists. This is faster, especially // for huge folders. for _, ext := range []string{"cue", "cuE", "cUe", "cUE", "Cue", "CuE", "CUe", "CUE"} { cs := pathNoext + "." + ext st, err := os.Stat(cs) if err != nil { continue } if st.Size() > cuesheetMaxsize { fr.warning.Printf("Cuesheet size %v > %v bytes, skipping", cs, cuesheetMaxsize) continue } buf, err := ioutil.ReadFile(cs) if err != nil { fr.warning.Print(err) continue } info.cuesheet, ErrCuesheet = cuesheet.New(buf) break } } // Remove cuesheet from tags to avoid printing it. delete(info.filetags, "cuesheet") // The number of tracks in current file is usually 1, it can be more if a // cuesheet is found. info.trackCount = 1 if ErrCuesheet == nil { // Copy the cuesheet header to the tags. Some entries appear both in the // header and in the track details. We map the cuesheet header entries to // the respective quivalent for FFmpeg tags. for k, v := range info.cuesheet.Header { switch k { case "PERFORMER": info.filetags["album_artist"] = v case "SONGWRITER": info.filetags["album_artist"] = v case "TITLE": info.filetags["album"] = v default: info.filetags[strings.ToLower(k)] = v } } // A cuesheet might have several FILE entries, or even none (non-standard). // In case of none, tracks are stored at file "" (the empty string) in the // Cuesheet structure. Otherwise, we find the most related file. base := stringNorm(filepath.Base(info.path)) max := 0.0 for f := range info.cuesheet.Files { r := stringRel(stringNorm(f), base) if r > max { max = r info.cuesheetFile = f } } info.trackCount = len(info.cuesheet.Files[info.cuesheetFile]) } // Set bitrate. // FFmpeg stores bitrate as a string, Demlo needs a number. If // 'streams[audioIndex].bit_rate' is empty (e.g. in APE files), look for // 'format.bit_rate'. To ease querying bitrate from user scripts, store it // in 'info.bitrate'. info.bitrate, err = strconv.Atoi(probed.Streams[info.audioIndex].Bitrate) if err != nil { info.bitrate, err = strconv.Atoi(probed.Format.Bitrate) if err != nil { fr.warning.Print("Cannot get bitrate from", info.path) return err } } return nil }