Example #1
0
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)
		}
	}
}
Example #2
0
// 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
}