Compare commits

..
10 Commits
Author SHA1 Message Date
Josip Tišljar Mataušić c414b552b3 make some ogg/vorbis/flac functions public
Go / build (push) Failing after 2m18s
2026-07-24 00:16:09 +02:00
Jorge JuniorandDavid Howden 3d75831295 id3v2: try to parse the year when 'year' tag could be a date (#104)
Co-authored-by: David Howden <dhowden@gmail.com>
2024-04-17 15:37:06 +10:00
yabobay dc579f508b Fallback to year field for ogg files when date not present (#105)
* Recognise year field for ogg files when present

* prioritize 'date' over 'year' tag on ogg files
2024-04-14 09:08:47 +10:00
Noah Zoschke 713ab0e946 add additional means (#101) 2024-01-23 08:42:04 +11:00
bretttolbert 0253fc576d added genre codes 148 to 191 (winamp ext) (#103)
* added genre codes 148 to 191 (winamp ext)
* make new genres captialization consistent with existing
* removed hyphen from Indie-Rock genre
2024-01-23 08:33:51 +11:00
Melvyn 978a0926ee vorbis: return artist instead of performer (#97)
* Return artist instead of performer for vorbis tags

Also fix possibly a mistake where the empty performer tag would be returned for the composer mathod.

* Add missing performer comment
2023-06-30 13:38:51 +10:00
David Howden adf36e8960 update README links (#93) 2022-06-19 09:00:19 +10:00
David Howden e66a190c9f update README (#92)
Fixes #85
2022-06-18 09:25:55 +10:00
David Howden d68341c4e2 Add go.yaml to trigger actions on pull requests (#84) 2022-06-18 09:22:08 +10:00
David Howden c3f9cefce8 go.mod: add go mod files (#91) 2022-06-18 09:16:05 +10:00
11 changed files with 136 additions and 61 deletions
+26
View File
@@ -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 ./...
+3 -4
View File
@@ -1,6 +1,5 @@
# MP3/MP4/OGG/FLAC metadata parsing library
[![Build Status](https://travis-ci.org/dhowden/tag.svg?branch=master)](https://travis-ci.org/dhowden/tag)
[![GoDoc](https://godoc.org/github.com/dhowden/tag?status.svg)](https://godoc.org/github.com/dhowden/tag)
[![GoDoc](https://pkg.go.dev/badge/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
+8 -8
View File
@@ -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
}
+7
View File
@@ -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
+4
View File
@@ -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=
+11
View File
@@ -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.
+3
View File
@@ -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
View File
@@ -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) {
+9 -2
View File
@@ -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
}
+8 -5
View File
@@ -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 {
+44 -40
View File
@@ -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
}