2015-03-19 13:21:53 +01:00
|
|
|
package tag
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha1"
|
|
|
|
"encoding/binary"
|
|
|
|
"fmt"
|
2015-06-30 00:58:55 +02:00
|
|
|
"hash"
|
2015-03-19 13:21:53 +01:00
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
)
|
|
|
|
|
2015-04-03 05:58:49 +02:00
|
|
|
// Sum creates a checksum of the audio file data provided by the io.ReadSeeker which is metadata
|
2015-03-30 13:50:20 +02:00
|
|
|
// (ID3, MP4) invariant.
|
2015-04-03 05:58:49 +02:00
|
|
|
func Sum(r io.ReadSeeker) (string, error) {
|
2015-03-19 13:21:53 +01:00
|
|
|
b, err := readBytes(r, 11)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2015-06-07 04:58:58 +02:00
|
|
|
_, err = r.Seek(-11, os.SEEK_CUR)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("could not seek back to original position: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-03-19 13:21:53 +01:00
|
|
|
if string(b[4:11]) == "ftypM4A" {
|
2015-04-03 05:58:49 +02:00
|
|
|
return SumAtoms(r)
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if string(b[0:3]) == "ID3" {
|
2015-04-03 05:58:49 +02:00
|
|
|
return SumID3v2(r)
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2015-04-03 05:58:49 +02:00
|
|
|
h, err := SumID3v1(r)
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
|
|
|
if err == ErrNotID3v1 {
|
2015-04-03 05:58:49 +02:00
|
|
|
return SumAll(r)
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return h, nil
|
|
|
|
}
|
|
|
|
|
2015-06-07 04:58:58 +02:00
|
|
|
// SumAll returns a checksum of the content from the reader (until EOF).
|
2015-04-03 05:58:49 +02:00
|
|
|
func SumAll(r io.ReadSeeker) (string, error) {
|
2015-06-30 00:58:55 +02:00
|
|
|
h := sha1.New()
|
|
|
|
_, err := io.Copy(h, r)
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
|
|
|
return "", nil
|
|
|
|
}
|
2015-06-30 00:58:55 +02:00
|
|
|
return hashSum(h), nil
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2015-04-03 05:58:49 +02:00
|
|
|
// 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) {
|
2015-03-19 13:21:53 +01:00
|
|
|
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)
|
2015-06-30 00:57:03 +02:00
|
|
|
_, err := r.Seek(4, os.SEEK_CUR)
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
fallthrough
|
|
|
|
|
|
|
|
case "moov", "udta", "ilst":
|
2015-06-07 04:58:58 +02:00
|
|
|
return SumAtoms(r)
|
2015-03-19 13:21:53 +01:00
|
|
|
|
|
|
|
case "mdat": // stop when we get to the data
|
2015-06-30 00:58:55 +02:00
|
|
|
h := sha1.New()
|
|
|
|
_, err := io.CopyN(h, r, int64(size-8))
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error reading audio data: %v", err)
|
|
|
|
}
|
2015-06-30 00:58:55 +02:00
|
|
|
return hashSum(h), nil
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = r.Seek(int64(size-8), os.SEEK_CUR)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error reading '%v' tag: %v", name, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-04-03 05:58:49 +02:00
|
|
|
// 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) {
|
2015-06-30 00:58:55 +02:00
|
|
|
// Need to stop before we hit potential ID3v1 data.
|
|
|
|
n, err := r.Seek(-128, os.SEEK_END)
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
2015-06-30 00:58:55 +02:00
|
|
|
return "", fmt.Errorf("error seeking to the end of the file (minus ID3v1 header): %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: improve this check???
|
|
|
|
if n <= 0 {
|
|
|
|
return "", fmt.Errorf("file size must be greater than 128 bytes (ID3v1 header size) for MP3")
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2015-06-30 00:58:55 +02:00
|
|
|
// Seek back to the original position now!
|
|
|
|
_, err = r.Seek(-1*n, os.SEEK_SET)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error seeking back to the start of the data: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
h := sha1.New()
|
|
|
|
_, err = io.CopyN(h, r, n)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error reading %v bytes: %v", n, err)
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
2015-06-30 00:58:55 +02:00
|
|
|
return hashSum(h), nil
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2015-04-16 00:36:35 +02:00
|
|
|
// SumID3v2 constructs a checksum of MP3 audio file data (assumed to have ID3v2 tags) provided by the
|
2015-03-30 13:50:20 +02:00
|
|
|
// io.ReadSeeker which is metadata invariant.
|
2015-04-03 05:58:49 +02:00
|
|
|
func SumID3v2(r io.ReadSeeker) (string, error) {
|
2015-06-30 00:58:55 +02:00
|
|
|
header, err := readID3v2Header(r)
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error reading ID3v2 header: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-06-30 00:58:55 +02:00
|
|
|
_, err = r.Seek(int64(header.Size)+10, os.SEEK_SET)
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error seeking to end of ID3V2 header: %v", err)
|
|
|
|
}
|
|
|
|
|
2015-06-30 00:58:55 +02:00
|
|
|
// Need to stop before we hit potential ID3v1 data.
|
|
|
|
n, err := r.Seek(-128, os.SEEK_END)
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
2015-06-30 00:58:55 +02:00
|
|
|
return "", fmt.Errorf("error seeking to the end of the file (minus ID3v1 header): %v", err)
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2015-06-30 00:58:55 +02:00
|
|
|
// TODO: remove this check?????
|
|
|
|
if n < 0 {
|
|
|
|
return "", fmt.Errorf("file size must be greater than 128 bytes for MP3: %v bytes", n)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Seek back to the original position now!
|
|
|
|
_, err = r.Seek(-1*n, os.SEEK_SET)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error seeking back to the start of the data: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
h := sha1.New()
|
|
|
|
_, err = io.CopyN(h, r, n)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error reading %v bytes: %v", n, err)
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
2015-06-30 00:58:55 +02:00
|
|
|
return hashSum(h), nil
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2015-06-30 00:58:55 +02:00
|
|
|
func hashSum(h hash.Hash) string {
|
|
|
|
return fmt.Sprintf("%x", h.Sum([]byte{}))
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|