Compare commits
10
Commits
77907a30b7
...
c414b552b3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c414b552b3 | ||
|
|
3d75831295 | ||
|
|
dc579f508b | ||
|
|
713ab0e946 | ||
|
|
0253fc576d | ||
|
|
978a0926ee | ||
|
|
adf36e8960 | ||
|
|
e66a190c9f | ||
|
|
d68341c4e2 | ||
|
|
c3f9cefce8 |
@@ -0,0 +1,26 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "1.20"
|
||||
cache: true
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
@@ -1,6 +1,5 @@
|
||||
# MP3/MP4/OGG/FLAC metadata parsing library
|
||||
[](https://travis-ci.org/dhowden/tag)
|
||||
[](https://godoc.org/github.com/dhowden/tag)
|
||||
[](https://pkg.go.dev/github.com/dhowden/tag)
|
||||
|
||||
This package provides MP3 (ID3v1,2.{2,3,4}) and MP4 (ACC, M4A, ALAC), OGG and FLAC metadata detection, parsing and artwork extraction.
|
||||
|
||||
@@ -47,14 +46,14 @@ type Metadata interface {
|
||||
This package also provides a metadata-invariant checksum for audio files: only the audio data is used to
|
||||
construct the checksum.
|
||||
|
||||
[http://godoc.org/github.com/dhowden/tag#Sum](http://godoc.org/github.com/dhowden/tag#Sum)
|
||||
[https://pkg.go.dev/github.com/dhowden/tag#Sum](https://pkg.go.dev/github.com/dhowden/tag#Sum)
|
||||
|
||||
## Tools
|
||||
|
||||
There are simple command-line tools which demonstrate basic tag extraction and summing:
|
||||
|
||||
```console
|
||||
$ go get github.com/dhowden/tag/...
|
||||
$ go install github.com/dhowden/tag/cmd/tag@latest
|
||||
$ cd $GOPATH/bin
|
||||
$ ./tag 11\ High\ Hopes.m4a
|
||||
Metadata Format: MP4
|
||||
|
||||
@@ -34,12 +34,12 @@ func ReadFLACTags(r io.ReadSeeker) (Metadata, error) {
|
||||
return nil, errors.New("expected 'fLaC'")
|
||||
}
|
||||
|
||||
m := &metadataFLAC{
|
||||
newMetadataVorbis(),
|
||||
m := &MetadataFLAC{
|
||||
NewMetadataVorbis(),
|
||||
}
|
||||
|
||||
for {
|
||||
last, err := m.readFLACMetadataBlock(r)
|
||||
last, err := m.ReadFLACMetadataBlock(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -51,11 +51,11 @@ func ReadFLACTags(r io.ReadSeeker) (Metadata, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type metadataFLAC struct {
|
||||
*metadataVorbis
|
||||
type MetadataFLAC struct {
|
||||
*MetadataVorbis
|
||||
}
|
||||
|
||||
func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
|
||||
func (m *MetadataFLAC) ReadFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
|
||||
blockHeader, err := readBytes(r, 1)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -73,7 +73,7 @@ func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err er
|
||||
|
||||
switch blockType(blockHeader[0]) {
|
||||
case vorbisCommentBlock:
|
||||
err = m.readVorbisComment(r)
|
||||
err = m.ReadVorbisComment(r)
|
||||
|
||||
case pictureBlock:
|
||||
err = m.readPictureBlock(r)
|
||||
@@ -84,6 +84,6 @@ func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err er
|
||||
return
|
||||
}
|
||||
|
||||
func (m *metadataFLAC) FileType() FileType {
|
||||
func (m *MetadataFLAC) FileType() FileType {
|
||||
return FLAC
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module github.com/dhowden/tag
|
||||
|
||||
go 1.20
|
||||
|
||||
require github.com/dhowden/itl v0.0.0-20170329215456-9fbe21093131
|
||||
|
||||
require github.com/dhowden/plist v0.0.0-20141002110153-5db6e0d9931a // indirect
|
||||
@@ -0,0 +1,4 @@
|
||||
github.com/dhowden/itl v0.0.0-20170329215456-9fbe21093131 h1:siEGb+iB1Ea75U7BnkYVSqSRzE6QHlXCbqEXenxRmhQ=
|
||||
github.com/dhowden/itl v0.0.0-20170329215456-9fbe21093131/go.mod h1:eVWQJVQ67aMvYhpkDwaH2Goy2vo6v8JCMfGXfQ9sPtw=
|
||||
github.com/dhowden/plist v0.0.0-20141002110153-5db6e0d9931a h1:7MucP9rMAsQRcRE1sGpvMZoTxFYZlDmfDvCH+z7H+90=
|
||||
github.com/dhowden/plist v0.0.0-20141002110153-5db6e0d9931a/go.mod h1:sLjdR6uwx3L6/Py8F+QgAfeiuY87xuYGwCDqRFrvCzw=
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Genre definitions 0-79 follow the ID3 tag specification of 1999.
|
||||
// More genres have been successively introduced in later Winamp versions.
|
||||
// https://en.wikipedia.org/wiki/List_of_ID3v1_genres#Extension_by_Winamp
|
||||
var id3v2Genres = [...]string{
|
||||
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
|
||||
"Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
|
||||
@@ -41,6 +44,14 @@ var id3v2Genres = [...]string{
|
||||
"Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian",
|
||||
"Christian Rock ", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
|
||||
"Synthpop",
|
||||
"Christmas", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat",
|
||||
"Chillout", "Downtempo", "Dub", "EBM", "Eclectic", "Electro",
|
||||
"Electroclash", "Emo", "Experimental", "Garage", "Global", "IDM",
|
||||
"Illbient", "Industro-Goth", "Jam Band", "Krautrock", "Leftfield", "Lounge",
|
||||
"Math Rock", "New Romantic", "Nu-Breakz", "Post-Punk", "Post-Rock", "Psytrance",
|
||||
"Shoegaze", "Space Rock", "Trop Rock", "World Music", "Neoclassical", "Audiobook",
|
||||
"Audio Theatre", "Neue Deutsche Welle", "Podcast", "Indie Rock", "G-Funk", "Dubstep",
|
||||
"Garage Rock", "Psybient",
|
||||
}
|
||||
|
||||
// id3v2Header is a type which represents an ID3v2 tag header.
|
||||
|
||||
@@ -141,6 +141,9 @@ func TestGenreExpension(t *testing.T) {
|
||||
"Test (17)": "Test Rock",
|
||||
"(17)(93)": "Rock Psychedelic Rock",
|
||||
"(17)Test(93)": "Rock Test Psychedelic Rock",
|
||||
"(175)": "Post-Punk",
|
||||
"(187)": "Indie Rock",
|
||||
"(191)": "Psybient",
|
||||
}
|
||||
for g, r := range tests {
|
||||
got := id3v2genre(g)
|
||||
|
||||
+13
-2
@@ -7,6 +7,7 @@ package tag
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type frameNames map[string][2]string
|
||||
@@ -89,8 +90,18 @@ func (m metadataID3v2) Genre() string {
|
||||
}
|
||||
|
||||
func (m metadataID3v2) Year() int {
|
||||
year, _ := strconv.Atoi(m.getString(frames.Name("year", m.Format())))
|
||||
return year
|
||||
stringYear := m.getString(frames.Name("year", m.Format()))
|
||||
|
||||
if year, err := strconv.Atoi(stringYear); err == nil {
|
||||
return year
|
||||
}
|
||||
|
||||
date, err := time.Parse(time.DateOnly, stringYear)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return date.Year()
|
||||
}
|
||||
|
||||
func parseXofN(s string) (x, n int) {
|
||||
|
||||
@@ -45,6 +45,12 @@ var atoms = atomNames(map[string]string{
|
||||
"disk": "disc",
|
||||
})
|
||||
|
||||
var means = map[string]bool{
|
||||
"com.apple.iTunes": true,
|
||||
"com.mixedinkey.mixedinkey": true,
|
||||
"com.serato.dj": true,
|
||||
}
|
||||
|
||||
// Detect PNG image if "implicit" class is used
|
||||
var pngHeader = []byte{137, 80, 78, 71, 13, 10, 26, 10}
|
||||
|
||||
@@ -226,7 +232,7 @@ func readAtomHeader(r io.ReadSeeker) (name string, size uint32, err error) {
|
||||
|
||||
// Generic atom.
|
||||
// Should have 3 sub atoms : mean, name and data.
|
||||
// We check that mean is "com.apple.iTunes" and we use the subname as
|
||||
// We check that mean is "com.apple.iTunes" or others and we use the subname as
|
||||
// the name, and move to the data atom.
|
||||
// Data atom could have multiple data values, each with a header.
|
||||
// If anything goes wrong, we jump at the end of the "----" atom.
|
||||
@@ -268,9 +274,10 @@ func readCustomAtom(r io.ReadSeeker, size uint32) (_ string, data []string, _ er
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if subNames["mean"] != "com.apple.iTunes" || subNames["name"] == "" || len(data) == 0 {
|
||||
if !means[subNames["mean"]] || subNames["name"] == "" || len(data) == 0 {
|
||||
return "----", nil, nil
|
||||
}
|
||||
|
||||
return subNames["name"], data, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,9 @@ func (o *oggDemuxer) Read(r io.Reader) ([][]byte, error) {
|
||||
var packets [][]byte
|
||||
var p int
|
||||
for _, s := range segmentTable {
|
||||
if !continued && !bytes.HasPrefix(segmentsData, vorbisCommentPrefix) && !bytes.HasPrefix(segmentsData, opusTagsPrefix) {
|
||||
return nil, fmt.Errorf("not a metadata segment")
|
||||
}
|
||||
packetBuf.Write(segmentsData[p : p+int(s)])
|
||||
if s < 255 {
|
||||
packets = append(packets, packetBuf.Bytes())
|
||||
@@ -150,15 +153,15 @@ func ReadOGGTags(r io.Reader) (Metadata, error) {
|
||||
switch {
|
||||
case bytes.HasPrefix(b, vorbisCommentPrefix):
|
||||
m := &metadataOGG{
|
||||
newMetadataVorbis(),
|
||||
NewMetadataVorbis(),
|
||||
}
|
||||
err = m.readVorbisComment(bytes.NewReader(b[len(vorbisCommentPrefix):]))
|
||||
err = m.ReadVorbisComment(bytes.NewReader(b[len(vorbisCommentPrefix):]))
|
||||
return m, err
|
||||
case bytes.HasPrefix(b, opusTagsPrefix):
|
||||
m := &metadataOGG{
|
||||
newMetadataVorbis(),
|
||||
NewMetadataVorbis(),
|
||||
}
|
||||
err = m.readVorbisComment(bytes.NewReader(b[len(opusTagsPrefix):]))
|
||||
err = m.ReadVorbisComment(bytes.NewReader(b[len(opusTagsPrefix):]))
|
||||
return m, err
|
||||
}
|
||||
}
|
||||
@@ -166,7 +169,7 @@ func ReadOGGTags(r io.Reader) (Metadata, error) {
|
||||
}
|
||||
|
||||
type metadataOGG struct {
|
||||
*metadataVorbis
|
||||
*MetadataVorbis
|
||||
}
|
||||
|
||||
func (m *metadataOGG) FileType() FileType {
|
||||
|
||||
@@ -15,18 +15,18 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func newMetadataVorbis() *metadataVorbis {
|
||||
return &metadataVorbis{
|
||||
func NewMetadataVorbis() *MetadataVorbis {
|
||||
return &MetadataVorbis{
|
||||
c: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
type metadataVorbis struct {
|
||||
type MetadataVorbis struct {
|
||||
c map[string]string // the vorbis comments
|
||||
p *Picture
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) readVorbisComment(r io.Reader) error {
|
||||
func (m *MetadataVorbis) ReadVorbisComment(r io.Reader) error {
|
||||
vendorLen, err := readUint32LittleEndian(r)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -70,7 +70,7 @@ func (m *metadataVorbis) readVorbisComment(r io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) readPictureBlock(r io.Reader) error {
|
||||
func (m *MetadataVorbis) readPictureBlock(r io.Reader) error {
|
||||
b, err := readInt(r, 4)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -156,11 +156,11 @@ func parseComment(c string) (k, v string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Format() Format {
|
||||
func (m *MetadataVorbis) Format() Format {
|
||||
return VORBIS
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Raw() map[string]interface{} {
|
||||
func (m *MetadataVorbis) Raw() map[string]interface{} {
|
||||
raw := make(map[string]interface{}, len(m.c))
|
||||
for k, v := range m.c {
|
||||
raw[k] = v
|
||||
@@ -168,11 +168,32 @@ func (m *metadataVorbis) Raw() map[string]interface{} {
|
||||
return raw
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Title() string {
|
||||
func (m *MetadataVorbis) Title() string {
|
||||
return m.c["title"]
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Artist() string {
|
||||
func (m *MetadataVorbis) Artist() string {
|
||||
// ARTIST
|
||||
// The artist generally considered responsible for the work. In popular music
|
||||
// this is usually the performing band or singer. For classical music it would
|
||||
// be the composer. For an audio book it would be the author of the original text.
|
||||
return m.c["artist"]
|
||||
}
|
||||
|
||||
func (m *MetadataVorbis) Album() string {
|
||||
return m.c["album"]
|
||||
}
|
||||
|
||||
func (m *MetadataVorbis) AlbumArtist() string {
|
||||
// This field isn't actually included in the standard, though
|
||||
// it is commonly assigned to albumartist.
|
||||
return m.c["albumartist"]
|
||||
}
|
||||
|
||||
func (m *MetadataVorbis) Composer() string {
|
||||
if m.c["composer"] != "" {
|
||||
return m.c["composer"]
|
||||
}
|
||||
// PERFORMER
|
||||
// The artist(s) who performed the work. In classical music this would be the
|
||||
// conductor, orchestra, soloists. In an audio book it would be the actor who
|
||||
@@ -184,41 +205,24 @@ func (m *metadataVorbis) Artist() string {
|
||||
return m.c["artist"]
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Album() string {
|
||||
return m.c["album"]
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) AlbumArtist() string {
|
||||
// This field isn't actually included in the standard, though
|
||||
// it is commonly assigned to albumartist.
|
||||
return m.c["albumartist"]
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Composer() string {
|
||||
// ARTIST
|
||||
// The artist generally considered responsible for the work. In popular music
|
||||
// this is usually the performing band or singer. For classical music it would
|
||||
// be the composer. For an audio book it would be the author of the original text.
|
||||
if m.c["composer"] != "" {
|
||||
return m.c["composer"]
|
||||
}
|
||||
if m.c["performer"] == "" {
|
||||
return ""
|
||||
}
|
||||
return m.c["artist"]
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Genre() string {
|
||||
func (m *MetadataVorbis) Genre() string {
|
||||
return m.c["genre"]
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Year() int {
|
||||
func (m *MetadataVorbis) Year() int {
|
||||
var dateFormat string
|
||||
|
||||
// The date need to follow the international standard https://en.wikipedia.org/wiki/ISO_8601
|
||||
// and obviously the VorbisComment standard https://wiki.xiph.org/VorbisComment#Date_and_time
|
||||
switch len(m.c["date"]) {
|
||||
case 0:
|
||||
// Fallback on year tag as some files use that.
|
||||
if len(m.c["year"]) != 0 {
|
||||
year, err := strconv.Atoi(m.c["year"])
|
||||
if err == nil {
|
||||
return year
|
||||
}
|
||||
}
|
||||
return 0
|
||||
case 4:
|
||||
dateFormat = "2006"
|
||||
@@ -232,31 +236,31 @@ func (m *metadataVorbis) Year() int {
|
||||
return t.Year()
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Track() (int, int) {
|
||||
func (m *MetadataVorbis) Track() (int, int) {
|
||||
x, _ := strconv.Atoi(m.c["tracknumber"])
|
||||
// https://wiki.xiph.org/Field_names
|
||||
n, _ := strconv.Atoi(m.c["tracktotal"])
|
||||
return x, n
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Disc() (int, int) {
|
||||
func (m *MetadataVorbis) Disc() (int, int) {
|
||||
// https://wiki.xiph.org/Field_names
|
||||
x, _ := strconv.Atoi(m.c["discnumber"])
|
||||
n, _ := strconv.Atoi(m.c["disctotal"])
|
||||
return x, n
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Lyrics() string {
|
||||
func (m *MetadataVorbis) Lyrics() string {
|
||||
return m.c["lyrics"]
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Comment() string {
|
||||
func (m *MetadataVorbis) Comment() string {
|
||||
if m.c["comment"] != "" {
|
||||
return m.c["comment"]
|
||||
}
|
||||
return m.c["description"]
|
||||
}
|
||||
|
||||
func (m *metadataVorbis) Picture() *Picture {
|
||||
func (m *MetadataVorbis) Picture() *Picture {
|
||||
return m.p
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user