6df4e78d96
Previously we would seek to the beginning of the io.ReadSeeker. It is much more flexible to assume that the caller passes the ReadSeeker in an appropriate state.
145 lines
3.2 KiB
Go
145 lines
3.2 KiB
Go
package tag
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
)
|
|
|
|
// Sum creates a checksum of the audio file data provided by the io.ReadSeeker which is metadata
|
|
// (ID3, MP4) invariant.
|
|
func Sum(r io.ReadSeeker) (string, error) {
|
|
b, err := readBytes(r, 11)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_, err = r.Seek(-11, os.SEEK_CUR)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not seek back to original position: %v", err)
|
|
}
|
|
|
|
if string(b[4:11]) == "ftypM4A" {
|
|
return SumAtoms(r)
|
|
}
|
|
|
|
if string(b[0:3]) == "ID3" {
|
|
return SumID3v2(r)
|
|
}
|
|
|
|
h, err := SumID3v1(r)
|
|
if err != nil {
|
|
if err == ErrNotID3v1 {
|
|
return SumAll(r)
|
|
}
|
|
return "", err
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
// SumAll returns a checksum of the content from the reader (until EOF).
|
|
func SumAll(r io.ReadSeeker) (string, error) {
|
|
b, err := ioutil.ReadAll(r)
|
|
if err != nil {
|
|
return "", nil
|
|
}
|
|
return sum(b), nil
|
|
}
|
|
|
|
// SumAtoms constructs a checksum of MP4 audio file data provided by the io.ReadSeeker which is
|
|
// metadata invariant.
|
|
func SumAtoms(r io.ReadSeeker) (string, error) {
|
|
for {
|
|
var size uint32
|
|
err := binary.Read(r, binary.BigEndian, &size)
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return "", fmt.Errorf("reached EOF before audio data")
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
name, err := readString(r, 4)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
switch name {
|
|
case "meta":
|
|
// next_item_id (int32)
|
|
_, err := readBytes(r, 4)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fallthrough
|
|
|
|
case "moov", "udta", "ilst":
|
|
return SumAtoms(r)
|
|
|
|
case "free":
|
|
_, err = r.Seek(int64(size-8), os.SEEK_CUR)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error reading 'free' space: %v", err)
|
|
}
|
|
continue
|
|
|
|
case "mdat": // stop when we get to the data
|
|
b, err := readBytes(r, int(size-8))
|
|
if err != nil {
|
|
return "", fmt.Errorf("error reading audio data: %v", err)
|
|
}
|
|
return sum(b), nil
|
|
}
|
|
|
|
_, err = r.Seek(int64(size-8), os.SEEK_CUR)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error reading '%v' tag: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SumID3v1 constructs a checksum of MP3 audio file data (assumed to have ID3v1 tags) provided
|
|
// by the io.ReadSeeker which is metadata invariant.
|
|
func SumID3v1(r io.ReadSeeker) (string, error) {
|
|
b, err := ioutil.ReadAll(r)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(b) < 128 {
|
|
return "", fmt.Errorf("file size must be greater than 128 bytes for ID3v1 metadata (size: %v)", len(b))
|
|
}
|
|
return sum(b[:len(b)-128]), nil
|
|
}
|
|
|
|
// SumID3v2 constructs a checksum of MP3 audio file data (assumed to have ID3v2 tags) provided by the
|
|
// io.ReadSeeker which is metadata invariant.
|
|
func SumID3v2(r io.ReadSeeker) (string, error) {
|
|
h, err := readID3v2Header(r)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error reading ID3v2 header: %v", err)
|
|
}
|
|
|
|
_, err = r.Seek(int64(h.Size)+10, os.SEEK_SET)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error seeking to end of ID3V2 header: %v", err)
|
|
}
|
|
|
|
b, err := ioutil.ReadAll(r)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error reading audio data: %v", err)
|
|
}
|
|
|
|
if len(b) < 128 {
|
|
return "", fmt.Errorf("file size must be greater than 128 bytes for MP3 (ID3v2 header size: %d, remaining: %d)", h.Size, len(b))
|
|
}
|
|
return sum(b[:len(b)-128]), nil
|
|
}
|
|
|
|
func sum(b []byte) string {
|
|
return fmt.Sprintf("%x", sha1.Sum(b))
|
|
}
|