144 lines
2.8 KiB
Go
144 lines
2.8 KiB
Go
|
package tag
|
||
|
|
||
|
import (
|
||
|
"crypto/sha1"
|
||
|
"encoding/binary"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
)
|
||
|
|
||
|
// Hash creates a hash of the audio file data provided by the io.ReadSeeker
|
||
|
// which ignores metadata (ID3, MP4) associated with the file.
|
||
|
func Hash(r io.ReadSeeker) (string, error) {
|
||
|
b, err := readBytes(r, 11)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
_, err = r.Seek(0, os.SEEK_SET)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
if string(b[4:11]) == "ftypM4A" {
|
||
|
return HashAtoms(r)
|
||
|
}
|
||
|
|
||
|
if string(b[0:3]) == "ID3" {
|
||
|
return HashID3v2(r)
|
||
|
}
|
||
|
|
||
|
h, err := HashID3v1(r)
|
||
|
if err != nil {
|
||
|
if err == ErrNotID3v1 {
|
||
|
return HashAll(r)
|
||
|
}
|
||
|
return "", err
|
||
|
}
|
||
|
return h, nil
|
||
|
}
|
||
|
|
||
|
// HashAll returns a hash of the entire content.
|
||
|
func HashAll(r io.ReadSeeker) (string, error) {
|
||
|
b, err := ioutil.ReadAll(r)
|
||
|
if err != nil {
|
||
|
return "", nil
|
||
|
}
|
||
|
return hash(b), nil
|
||
|
}
|
||
|
|
||
|
func HashAtoms(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 HashAtoms(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 hash(b), nil
|
||
|
}
|
||
|
|
||
|
_, err = r.Seek(int64(size-8), os.SEEK_CUR)
|
||
|
if err != nil {
|
||
|
return "", fmt.Errorf("error reading '%v' tag: %v", name, err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func HashID3v1(r io.ReadSeeker) (string, error) {
|
||
|
_, err := r.Seek(0, os.SEEK_SET)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
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 hash(b[:len(b)-128]), nil
|
||
|
}
|
||
|
|
||
|
func HashID3v2(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), 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 hash(b[:len(b)-128]), nil
|
||
|
}
|
||
|
|
||
|
func hash(b []byte) string {
|
||
|
return fmt.Sprintf("%x", sha1.Sum(b))
|
||
|
}
|