// HasID3V1Tag returns true if an ID3v1 tag is present at the end of r. func HasID3v1Tag(r types.SizeReaderAt) (bool, error) { if r.Size() < ID3v1TagLength { return false, nil } buf := make([]byte, len(id3v1Magic), len(id3v1Magic)) if _, err := r.ReadAt(buf, r.Size()-ID3v1TagLength); err != nil { return false, fmt.Errorf("Failed to read ID3v1 data: %v", err) } if bytes.Equal(buf, id3v1Magic) { return true, nil } return false, nil }
// indexMusic adds mutations to index the wholeRef by attached metadata and other properties. func indexMusic(r types.SizeReaderAt, wholeRef blob.Ref, mm *mutationMap) { tag, err := taglib.Decode(r, r.Size()) if err != nil { log.Print("index: error parsing tag: ", err) return } var footerLength int64 = 0 if hasTag, err := media.HasID3v1Tag(r); err != nil { log.Print("index: unable to check for ID3v1 tag: ", err) return } else if hasTag { footerLength = media.ID3v1TagLength } // Generate a hash of the audio portion of the file (i.e. excluding ID3v1 and v2 tags). audioStart := int64(tag.TagSize()) audioSize := r.Size() - audioStart - footerLength hash := sha1.New() if _, err := io.Copy(hash, io.NewSectionReader(r, audioStart, audioSize)); err != nil { log.Print("index: error generating SHA1 from audio data: ", err) return } mediaRef := blob.RefFromHash(hash) duration, err := media.GetMPEGAudioDuration(io.NewSectionReader(r, audioStart, audioSize)) if err != nil { log.Print("index: unable to calculate audio duration: ", err) duration = 0 } var yearStr, trackStr, discStr, durationStr string if !tag.Year().IsZero() { const justYearLayout = "2006" yearStr = tag.Year().Format(justYearLayout) } if tag.Track() != 0 { trackStr = fmt.Sprintf("%d", tag.Track()) } if tag.Disc() != 0 { discStr = fmt.Sprintf("%d", tag.Disc()) } if duration != 0 { durationStr = fmt.Sprintf("%d", duration/time.Millisecond) } // Note: if you add to this map, please update // pkg/search/query.go's MediaTagConstraint Tag docs. tags := map[string]string{ "title": tag.Title(), "artist": tag.Artist(), "album": tag.Album(), "genre": tag.Genre(), "musicbrainzalbumid": tag.CustomFrames()["MusicBrainz Album Id"], "year": yearStr, "track": trackStr, "disc": discStr, "mediaref": mediaRef.String(), "durationms": durationStr, } for tag, value := range tags { if value != "" { mm.Set(keyMediaTag.Key(wholeRef, tag), keyMediaTag.Val(value)) } } }
// GetMPEGAudioDuration reads the first frame in r and returns the audio length with millisecond precision. // Format details are at http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header. func GetMPEGAudioDuration(r types.SizeReaderAt) (time.Duration, error) { var header uint32 if err := binary.Read(io.NewSectionReader(r, 0, r.Size()), binary.BigEndian, &header); err != nil { return 0, fmt.Errorf("Failed to read MPEG frame header: %v", err) } getBits := func(startBit, numBits uint) uint32 { return (header << startBit) >> (32 - numBits) } if getBits(0, 11) != 0x7ff { return 0, errors.New("Missing sync bits in MPEG frame header") } var version mpegVersion var ok bool if version, ok = mpegVersionsById[getBits(11, 2)]; !ok { return 0, errors.New("Invalid MPEG version index") } var layer mpegLayer if layer, ok = mpegLayersByIndex[getBits(13, 2)]; !ok { return 0, errors.New("Invalid MPEG layer index") } bitrate := mpegBitrates[version][layer][getBits(16, 4)] if bitrate == 0 { return 0, errors.New("Invalid MPEG bitrate") } samplingRate := mpegSamplingRates[version][getBits(20, 2)] if samplingRate == 0 { return 0, errors.New("Invalid MPEG sample rate") } samplesPerFrame := mpegSamplesPerFrame[version][layer] var xingHeaderStart int64 = 4 // Skip "side information". if getBits(24, 2) == 0x3 { // Channel mode; 0x3 is mono. xingHeaderStart += 17 } else { xingHeaderStart += 32 } // Skip 16-bit CRC if present. if getBits(15, 1) == 0x0 { // 0x0 means "has protection". xingHeaderStart += 2 } b := make([]byte, 12, 12) if _, err := r.ReadAt(b, xingHeaderStart); err != nil { return 0, fmt.Errorf("Unable to read Xing header at %d: %v", xingHeaderStart, err) } var ms int64 if bytes.Equal(b[0:4], xingHeaderName) || bytes.Equal(b[0:4], infoHeaderName) { r := bytes.NewReader(b[4:]) var xingFlags uint32 binary.Read(r, binary.BigEndian, &xingFlags) if xingFlags&0x1 == 0x0 { return 0, fmt.Errorf("Xing header at %d lacks number of frames", xingHeaderStart) } var numFrames uint32 binary.Read(r, binary.BigEndian, &numFrames) ms = int64(samplesPerFrame) * int64(numFrames) * 1000 / int64(samplingRate) } else { // Okay, no Xing VBR header. Assume that the file has a constant bitrate. // (The other alternative is to read the whole file and examine each frame.) ms = r.Size() / int64(bitrate) * 8 } return time.Duration(ms) * time.Millisecond, nil }