Skip to content

Commit

Permalink
fixed id3 tags.
Browse files Browse the repository at this point in the history
need to fix iso support.
  • Loading branch information
bengarrett committed Aug 11, 2024
1 parent 6e1856f commit bdb6c3b
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 36 deletions.
6 changes: 4 additions & 2 deletions handler/app/internal/mf/mf.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ func (e *entry) parse(path, platform string, info fs.FileInfo) bool {
fmt.Fprintf(io.Discard, "ignore this error, %v", err)
return skipEntry
}
platform = strings.TrimSpace(platform)
e.image = isImage(e.sign)
e.text = isText(e.sign)
e.program = isProgram(e.sign, platform)
Expand All @@ -201,7 +202,9 @@ func (e *entry) parse(path, platform string, info fs.FileInfo) bool {
return e.parseProgram(path)
case e.sign == magicnumber.MusicModule:
return e.parseMusicMod(path)
case e.sign == magicnumber.MPEG1AudioLayer3:
case
e.sign == magicnumber.MPEG1AudioLayer3,
platform == tags.Audio.String():
return e.parseMusicID3(path)
}
return !skipEntry
Expand Down Expand Up @@ -257,7 +260,6 @@ func (e *entry) parseMusicID3(path string) bool {
return !skipEntry
}
}

// ID3 v1 tags are located at the end of the file.
id3v1, _ := os.Open(path)
if id3v1 == nil {
Expand Down
108 changes: 79 additions & 29 deletions internal/magicnumber/id3.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
// ID3v1Size is the minimum buffer size of an ID3 v1 tag.
const ID3v1Size = 128

// MusicID3v1 reads the [ID3 v1] tag in the byte slice and returns the title and artist.
const nul = "\x00"

// MusicID3v1 reads the [ID3 v1] tag in the byte slice and returns the song, artist and year.
// The ID3 v1 tag is a 128 byte tag at the end of an MP3 audio file.
//
// [ID3 v1]: http://id3.org/ID3v1
Expand All @@ -22,11 +24,11 @@ func MusicID3v1(p []byte) string {
if !bytes.Equal(p[0:3], []byte{'T', 'A', 'G'}) {
return ""
}
song := string(bytes.Trim(p[3:33], "\x00"))
song := string(bytes.Trim(p[3:33], nul))
song = strings.TrimSpace(song)
artist := string(bytes.Trim(p[33:63], "\x00"))
artist := string(bytes.Trim(p[33:63], nul))
artist = strings.TrimSpace(artist)
year := string(bytes.Trim(p[93:97], "\x00"))
year := string(bytes.Trim(p[93:97], nul))
year = strings.TrimSpace(year)
s := song
if artist != "" {
Expand All @@ -38,18 +40,10 @@ func MusicID3v1(p []byte) string {
return strings.TrimSpace(s)
}

func headerSize(p []byte) int {
const length = 10
if len(p) < length {
return 0
}
b0 := int(p[6]) * 2097152
b1 := int(p[7]) * 16384
b2 := int(p[8]) * 128
b3 := int(p[9]) * 1
return b0 + b1 + b2 + b3
}

// MusicID3v2 reads the [ID3 v2] tag in the byte slice and returns the song, artist and year.
// The ID3 v2 tag is a variable length tag at the start of an MP3 audio file.
//
// [ID3 v2]: https://id3.org/id3v2-00
func MusicID3v2(p []byte) string {
const length = 10
if len(p) < length {
Expand All @@ -58,45 +52,100 @@ func MusicID3v2(p []byte) string {
if !bytes.Equal(p[0:3], []byte{'I', 'D', '3'}) {
return ""
}
hsize := headerSize(p)
if hsize+length > len(p) {
if length > len(p) {
return ""
}
data := p[length : length+hsize]
data := p[length:]
switch version := p[3]; version {
case 0x2:
return "ID3v2.2"
return ID3v220(data...)
case 0x3:
return ID3v230(data...)
case 0x4:
return "ID3v2.4"
return ID3v230(data...)
}
return ""
}

// ID3v220 reads the [ID3 v2.2] tags in the byte slice and returns the song, artist and year.
// The v2.2 tag is obsolete but still found in the wild.
//
// [ID3 v2.2]: https://id3.org/id3v2-00
func ID3v220(data ...byte) string {
albumTitle := [3]byte{'T', 'A', 'L'}
leadPerformer := [3]byte{'T', 'P', '1'}
band := [3]byte{'T', 'P', '2'}
songName := [3]byte{'T', 'T', '2'}
year := [3]byte{'T', 'Y', 'E'}
s := ID3v22Frame(songName, data...)
if s != "" {
if lp := ID3v22Frame(leadPerformer, data...); lp != "" {
s += fmt.Sprintf(" by %s", lp)
} else if band := ID3v22Frame(band, data...); band != "" {
s += fmt.Sprintf(" by %s", band)
}
} else if ab := ID3v22Frame(albumTitle, data...); ab == "" {
return ""
}
if y := ID3v22Frame(year, data...); y != "" {
s += fmt.Sprintf(" (%s)", y)
}
return strings.TrimSpace(s)
}

// ID3v22Frame reads the ID3 v2.2 frame in the byte slice and returns the frame data as a string.
// The frame header contains a 3 byte identifier followed by a 3 byte size.
func ID3v22Frame(id [3]byte, data ...byte) string {
const header = 6
frameID := []byte{id[0], id[1], id[2]}
offset := bytes.Index(data, frameID)
if offset == -1 || offset+10 > len(data) {
return ""
}
b0 := int(data[offset+3]) * 16384
b1 := int(data[offset+4]) * 128
b2 := int(data[offset+5])
frameLen := b0 + b1 + b2
if offset+header+frameLen > len(data) {
return ""
}
b := bytes.Trim(data[offset+header:offset+header+frameLen], nul)
return strings.TrimSpace(string(b))
}

// TODO: handle non-ascii characters, look for extended iso-8859-1 1 byte characters and replace them with utf-8.
// https://en.wikipedia.org/wiki/ISO/IEC_8859-1

// ID3v230 reads the [ID3 v2.3] and ID3 v2.4 tags in the byte slice and returns the song, artist and year.
// The v2.3 and v2.4 tags are the most common ID3 tags found in MP3 files.
// For our purposes, we treat v2.3 and v2.4 tags the same as there's no difference for the metadata used.
//
// [ID3 v2.3]: https://id3.org/id3v2.3.0
func ID3v230(data ...byte) string {
ablumTitle := [4]byte{'T', 'A', 'L', 'B'}
albumTitle := [4]byte{'T', 'A', 'L', 'B'}
leadPerformer := [4]byte{'T', 'P', 'E', '1'}
contentGroup := [4]byte{'T', 'I', 'T', '1'}
songName := [4]byte{'T', 'I', 'T', '2'}
year := [4]byte{'T', 'Y', 'E', 'R'}
s := ID3v2Frame(songName, data...)
s := ID3v23Frame(songName, data...)
if s != "" {
if lp := ID3v2Frame(leadPerformer, data...); lp != "" {
if lp := ID3v23Frame(leadPerformer, data...); lp != "" {
s += fmt.Sprintf(" by %s", lp)
} else if cg := ID3v2Frame(contentGroup, data...); cg != "" {
} else if cg := ID3v23Frame(contentGroup, data...); cg != "" {
s += fmt.Sprintf(" by %s", cg)
}
} else if ab := ID3v2Frame(ablumTitle, data...); ab == "" {
} else if ab := ID3v23Frame(albumTitle, data...); ab == "" {
return ""
}
if y := ID3v2Frame(year, data...); y != "" {
if y := ID3v23Frame(year, data...); y != "" {
s += fmt.Sprintf(" (%s)", y)
}
return strings.TrimSpace(s)
}

func ID3v2Frame(id [4]byte, data ...byte) string {
// ID3v23Frame reads the ID3 v2.3 and v2.4 frame in the byte slice and returns the frame data as a string.
// The frame header contains a 4 byte identifier followed by a 4 byte size.
func ID3v23Frame(id [4]byte, data ...byte) string {
const header = 10
frameID := []byte{id[0], id[1], id[2], id[3]}
offset := bytes.Index(data, frameID)
Expand All @@ -111,5 +160,6 @@ func ID3v2Frame(id [4]byte, data ...byte) string {
if offset+header+frameLen > len(data) {
return ""
}
return strings.TrimSpace(string(data[offset+header : offset+header+frameLen]))
b := bytes.Trim(data[offset+header:offset+header+frameLen], nul)
return strings.TrimSpace(string(b))
}
6 changes: 5 additions & 1 deletion internal/magicnumber/media.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package magicnumber

import "bytes"
import (
"bytes"
)

// Package file media.go contains the functions that parse bytes as commom image, digital audio and video formats.
// A number of these media containers could support multiple modes,
Expand Down Expand Up @@ -175,6 +177,8 @@ func Mp4(p []byte) bool {
}

// Mp3 matches the MPEG-1 Audio Layer 3 audio format in the byte slice.
// This only checks for the ID3v2 tag and not the audio data.
// Songs with no ID3v2 tag will not be detected including files with ID3v1 tags.
func Mp3(p []byte) bool {
const min = 3
if len(p) < min {
Expand Down
9 changes: 5 additions & 4 deletions internal/magicnumber/synthesismusic.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,16 @@ func MusicMK(p []byte) string {
case
bytes.Equal(modHeader, []byte{'M', '.', 'K', '.'}),
bytes.Equal(modHeader, []byte{'M', '!', 'K', '!'}),
bytes.Equal(modHeader, []byte{'F', 'L', 'T', '4'}),
bytes.Equal(modHeader, []byte{'4', 'C', 'H', 'N'}):
bytes.Equal(modHeader, []byte{'4', 'C', 'H', 'N'}),
bytes.Equal(modHeader, []byte{'F', 'L', 'T', '4'}):
return music4Chan(p[0:healder])
case
bytes.Equal(modHeader, []byte{'6', 'C', 'H', 'N'}):
return music6Chan(p[0:healder])
case
bytes.Equal(modHeader, []byte{'8', 'C', 'H', 'N'}),
bytes.Equal(modHeader, []byte{'O', 'C', 'T', 'A'}):
bytes.Equal(modHeader, []byte{'F', 'L', 'T', '8'}),
bytes.Equal(modHeader, []byte{'O', 'C', 'T', 'A'}),
bytes.Equal(modHeader, []byte{'8', 'C', 'H', 'N'}):
return music8Chan(p[0:healder])
default:
return ""
Expand Down

0 comments on commit bdb6c3b

Please sign in to comment.