Browse Source

Procrastinated really hard on ID3v2 writing support...

main
Unbewohnte 3 years ago
parent
commit
db26a173f8
  1. 25
      util/conversion.go
  2. 8
      util/read.go
  3. 5
      v1/constants.go
  4. 7
      v1/errors.go
  5. 118
      v1/read.go
  6. 29
      v1/tag.go
  7. 7
      v2/errors.go
  8. 4
      v2/frame.go
  9. 199
      v2/header.go

25
util/conversion.go

@ -1,8 +1,6 @@
package util package util
import ( import (
"fmt"
"strconv"
"strings" "strings"
euni "golang.org/x/text/encoding/unicode" euni "golang.org/x/text/encoding/unicode"
@ -12,18 +10,19 @@ import (
const first7BitsMask = uint32(254) << 24 // shifting 11111110 to the end of uint32 const first7BitsMask = uint32(254) << 24 // shifting 11111110 to the end of uint32
// Decodes given byte into integer // Converts given bytes into integer
func ByteToInt(gByte byte) (int, error) { func BytesToInt(gBytes []byte) uint32 {
integer, err := strconv.Atoi(fmt.Sprintf("%d", gByte)) var integer uint32 = 0
if err != nil { for _, b := range gBytes {
return 0, err integer = integer << 8
integer = integer | uint32(b)
} }
return integer, nil return integer
} }
// Decodes given integer bytes into integer, ignores the first bit // Decodes given integer bytes into integer, ignores the first bit
// of every given byte in binary form // of every given byte in binary form
func BytesToIntIgnoreFirstBit(gBytes []byte) uint32 { func BytesToIntSynchsafe(gBytes []byte) uint32 {
var integer uint32 = 0 var integer uint32 = 0
for _, b := range gBytes { for _, b := range gBytes {
integer = integer << 7 integer = integer << 7
@ -34,14 +33,14 @@ func BytesToIntIgnoreFirstBit(gBytes []byte) uint32 {
} }
// The exact opposite of what `BytesToIntIgnoreFirstBit` does // The exact opposite of what `BytesToIntIgnoreFirstBit` does
func IntToBytesFirstBitZeroed(gInt uint32) []byte { func SynchsafeIntToBytes(gInt uint32) []byte {
bytes := make([]byte, 32) bytes := make([]byte, 32)
// looping 4 times (32 bits / 8 bits (4 bytes in int32)) // looping 4 times (32 bits / 8 bits (4 bytes in int32))
for i := 0; i < 32; i += 8 { for i := 0; i < 32; i += 8 {
gIntCopy := gInt //ie: 11010100 11001011 00100000 10111111 gIntCopy := gInt //11010101 11001011 00100000 10111111
first7 := gIntCopy & first7BitsMask first7 := gIntCopy & first7BitsMask //11010100 00000000 00000000 00000000
shifted := first7 >> 25 // 00000000 00000000 00000000 01101010 shifted := first7 >> 25 //00000000 00000000 00000000 01101010
bytes = append(bytes, byte(shifted)) bytes = append(bytes, byte(shifted))
} }

8
util/read.go

@ -6,9 +6,9 @@ import (
) )
// Shortcut function to read n bytes from reader. The general idea peeked from here: https://github.com/dhowden/tag/blob/master/util.go // Shortcut function to read n bytes from reader. The general idea peeked from here: https://github.com/dhowden/tag/blob/master/util.go
func Read(rs io.Reader, n uint64) ([]byte, error) { func Read(r io.Reader, n uint64) ([]byte, error) {
read := make([]byte, n) read := make([]byte, n)
_, err := rs.Read(read) _, err := r.Read(read)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not read from reader: %s", err) return nil, fmt.Errorf("could not read from reader: %s", err)
} }
@ -18,9 +18,9 @@ func Read(rs io.Reader, n uint64) ([]byte, error) {
// Reads from rs and conversts read []byte into string, ignoring all non-printable or // Reads from rs and conversts read []byte into string, ignoring all non-printable or
// invalid characters. // invalid characters.
func ReadToString(rs io.Reader, n uint64) (string, error) { func ReadToString(r io.Reader, n uint64) (string, error) {
read := make([]byte, n) read := make([]byte, n)
_, err := rs.Read(read) _, err := r.Read(read)
if err != nil { if err != nil {
return "", fmt.Errorf("could not read from reader: %s", err) return "", fmt.Errorf("could not read from reader: %s", err)
} }

5
v1/constants.go

@ -1,8 +1,11 @@
package v1 package v1
const ID3v1IDENTIFIER string = "TAG" const ID3v1IDENTIFIER string = "TAG"
const ID3v1SIZE int = 128 // bytes const ID3v1SIZE int = 128
const ID3v1INVALIDGENRE int = 255 const ID3v1INVALIDGENRE int = 255
const ID3v1ENHANCEDIDENTIFIER string = "TAG+"
const ENHANCEDSIZE int = 227
const V1_0 string = "ID3v1.0" const V1_0 string = "ID3v1.0"
const V1_1 string = "ID3v1.1" const V1_1 string = "ID3v1.1"

7
v1/errors.go

@ -0,0 +1,7 @@
package v1
// Exported ID3v1-specific errors
import "fmt"
var ErrDoesNotUseID3v1 error = fmt.Errorf("does not use ID3v1")

118
v1/read.go

@ -9,12 +9,114 @@ import (
"github.com/Unbewohnte/id3ed/util" "github.com/Unbewohnte/id3ed/util"
) )
var ErrDoesNotUseID3v1 error = fmt.Errorf("does not use ID3v1") var errDoesNotUseEnhancedID3v1 error = fmt.Errorf("does not use enhanced ID3v1 tag")
// Retrieves ID3v1 field values of provided io.ReadSeeker (usually a file) // Checks if enhanced tag is used
func usesEnhancedTag(rs io.ReadSeeker) bool {
_, err := rs.Seek(-int64(ID3v1SIZE+ENHANCEDSIZE), io.SeekEnd)
if err != nil {
return false
}
identifier, err := util.Read(rs, 4)
if err != nil {
return false
}
if !bytes.Equal(identifier, []byte(ID3v1ENHANCEDIDENTIFIER)) {
return false
}
return true
}
// Tries to read enhanced ID3V1 tag from rs
func readEnhancedTag(rs io.ReadSeeker) (EnhancedID3v1Tag, error) {
if !usesEnhancedTag(rs) {
return EnhancedID3v1Tag{}, errDoesNotUseEnhancedID3v1
}
var enhanced EnhancedID3v1Tag
// set reader into the position
_, err := rs.Seek(-int64(ID3v1SIZE+ENHANCEDSIZE), io.SeekEnd)
if err != nil {
return enhanced, fmt.Errorf("could not seek: %s", err)
}
// songname
songName, err := util.ReadToString(rs, 60)
if err != nil {
return EnhancedID3v1Tag{}, err
}
enhanced.SongName = songName
artist, err := util.ReadToString(rs, 60)
if err != nil {
return enhanced, err
}
enhanced.Artist = artist
// album
album, err := util.ReadToString(rs, 60)
if err != nil {
return enhanced, err
}
enhanced.Album = album
// speed
speedByte, err := util.Read(rs, 1)
if err != nil {
return enhanced, err
}
var speed string
switch speedByte[0] {
case 0:
speed = "Unset"
case 1:
speed = "Slow"
case 2:
speed = "Medium"
case 3:
speed = "Fast"
case 4:
speed = "Hardcore"
}
enhanced.Speed = speed
// genre
genre, err := util.ReadToString(rs, 30)
if err != nil {
return enhanced, err
}
enhanced.Genre = genre
// time
startTime, err := util.ReadToString(rs, 6)
if err != nil {
return enhanced, err
}
enhanced.StartTime = startTime
endtime, err := util.ReadToString(rs, 6)
if err != nil {
return enhanced, err
}
enhanced.EndTime = endtime
return enhanced, nil
}
// Retrieves ID3v1 field values from rs.
func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) { func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) {
var tag ID3v1Tag var tag ID3v1Tag
// check if uses enhanced tag
if usesEnhancedTag(rs) {
enhanced, _ := readEnhancedTag(rs)
tag.EnhancedTag = enhanced
}
// set reader to the last 128 bytes // set reader to the last 128 bytes
_, err := rs.Seek(-int64(ID3v1SIZE), io.SeekEnd) _, err := rs.Seek(-int64(ID3v1SIZE), io.SeekEnd)
if err != nil { if err != nil {
@ -76,10 +178,8 @@ func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) {
// check if 29th byte is null byte (v1.0 or v1.1) // check if 29th byte is null byte (v1.0 or v1.1)
if comment[28] == 0 { if comment[28] == 0 {
// it is v1.1, track number exists // it is v1.1, track number exists
track, err = util.ByteToInt(comment[29]) track = int(comment[29])
if err != nil {
return nil, fmt.Errorf("could not get int from byte: %s", err)
}
tag.Track = uint8(track) tag.Track = uint8(track)
comment = comment[0:28] comment = comment[0:28]
@ -91,10 +191,8 @@ func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
genreInt, err := util.ByteToInt(genreByte[0]) genreInt := int(genreByte[0])
if err != nil {
return nil, fmt.Errorf("cannot convert bytes to int: %s", err)
}
genre, exists := id3v1genres[int(genreInt)] genre, exists := id3v1genres[int(genreInt)]
if !exists { if !exists {
genre = "" genre = ""

29
v1/tag.go

@ -3,12 +3,25 @@ package v1
// https://id3.org/ID3v1 - documentation // https://id3.org/ID3v1 - documentation
type ID3v1Tag struct { type ID3v1Tag struct {
version string version string
SongName string SongName string
Artist string Artist string
Album string Album string
Year int Year int
Comment string Comment string
Track uint8 // basically a byte, but converted to int for convenience Track uint8 // basically a byte, but converted to int for convenience
Genre string Genre string
EnhancedTag EnhancedID3v1Tag
}
// from https://en.wikipedia.org/wiki/ID3
type EnhancedID3v1Tag struct {
SongName string
Artist string
Album string
Speed string
Genre string
StartTime string
EndTime string
} }

7
v2/errors.go

@ -0,0 +1,7 @@
package v2
// Exported ID3v2-specific errors
import "fmt"
var ErrDoesNotUseID3v2 error = fmt.Errorf("does not use ID3v2")

4
v2/frame.go

@ -63,7 +63,7 @@ func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) {
} }
header.ID = string(fHeaderbytes[0:3]) header.ID = string(fHeaderbytes[0:3])
framesizeBytes := util.BytesToIntIgnoreFirstBit(fHeaderbytes[3:6]) framesizeBytes := util.BytesToIntSynchsafe(fHeaderbytes[3:6])
header.Size = framesizeBytes header.Size = framesizeBytes
case V2_3: case V2_3:
@ -82,7 +82,7 @@ func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) {
// Size // Size
framesizeBytes := fHeaderbytes[4:8] framesizeBytes := fHeaderbytes[4:8]
framesize := util.BytesToIntIgnoreFirstBit(framesizeBytes) framesize := util.BytesToIntSynchsafe(framesizeBytes)
header.Size = framesize header.Size = framesize

199
v2/header.go

@ -8,24 +8,177 @@ import (
"github.com/Unbewohnte/id3ed/util" "github.com/Unbewohnte/id3ed/util"
) )
var ErrDoesNotUseID3v2 error = fmt.Errorf("does not use ID3v2") // Main header`s flags
type HeaderFlags struct { type HeaderFlags struct {
Unsynchronisated bool Unsynchronisated bool
Compressed bool
HasExtendedHeader bool HasExtendedHeader bool
Experimental bool Experimental bool
FooterPresent bool FooterPresent bool
} }
// ID3v2.x header structure // ID3v2.x`s main header structure
type Header struct { type Header struct {
Identifier string Identifier string
Flags HeaderFlags Flags HeaderFlags
Version string Version string
Size uint32 Size uint32
ExtendedHeader ExtendedHeader
}
// extended header`s flags
type ExtendedHeaderFlags struct {
UpdateTag bool
CRCpresent bool
HasRestrictions bool
Restrictions byte // a `lazy` approach :), just for now, maybe...
}
type ExtendedHeader struct {
Size uint32
Flags ExtendedHeaderFlags
PaddingSize uint32
CRCdata []byte
}
// Reads and structuralises extended header. Must
// be called AFTER the main header has beeen read (does not seek).
// ALSO ISN`T TESTED !!!
func (h *Header) readExtendedHeader(r io.Reader) error {
h.ExtendedHeader = ExtendedHeader{}
if !h.Flags.HasExtendedHeader {
return nil
}
var extended ExtendedHeader
// extended size
extendedSize, err := util.Read(r, 4)
if err != nil {
return fmt.Errorf("could not read from reader: %s", err)
}
switch h.Version {
case V2_3:
extended.Size = util.BytesToInt(extendedSize)
case V2_4:
extended.Size = util.BytesToIntSynchsafe(extendedSize)
}
// extended flags
switch h.Version {
case V2_3:
extendedFlag, err := util.Read(r, 2) // reading flag byte and a null-byte after
if err != nil {
return fmt.Errorf("could not read from reader: %s", err)
}
flagbits := fmt.Sprintf("%08b", extendedFlag[0])
if flagbits[0] == 1 {
extended.Flags.CRCpresent = true
} else {
extended.Flags.CRCpresent = false
}
case V2_4:
// skipping `Number of flag bytes` because it`s always `1`
_, err := util.Read(r, 1)
if err != nil {
return fmt.Errorf("could not read from reader: %s", err)
}
flagByte, err := util.Read(r, 1)
if err != nil {
return fmt.Errorf("could not read from reader: %s", err)
}
flagBits := fmt.Sprintf("%08b", flagByte[0])
if flagBits[1] == 1 {
extended.Flags.UpdateTag = true
} else {
extended.Flags.UpdateTag = false
}
if flagBits[2] == 1 {
extended.Flags.CRCpresent = true
} else {
extended.Flags.CRCpresent = false
}
if flagBits[3] == 1 {
extended.Flags.HasRestrictions = true
} else {
extended.Flags.HasRestrictions = false
}
}
// extracting data given by flags
switch h.Version {
case V2_3:
if extended.Flags.CRCpresent {
crcData, err := util.Read(r, 4)
if err != nil {
return fmt.Errorf("could not read from reader: %s", err)
}
extended.CRCdata = crcData
}
case V2_4:
// `Each flag that is set in the extended header has data attached`
if extended.Flags.UpdateTag {
// skipping null-byte length of `UpdateTag`
_, err := util.Read(r, 1)
if err != nil {
return fmt.Errorf("could not read from reader: %s", err)
}
}
if extended.Flags.CRCpresent {
crclen, err := util.Read(r, 1)
if err != nil {
return fmt.Errorf("could not read from reader: %s", err)
}
crcData, err := util.Read(r, uint64(crclen[0]))
if err != nil {
return fmt.Errorf("could not read from reader: %s", err)
}
extended.CRCdata = crcData
}
if extended.Flags.HasRestrictions {
// skipping one-byte length of `Restrictions`, because it`s always `1`
_, err := util.Read(r, 1)
if err != nil {
return fmt.Errorf("could not read from reader: %s", err)
}
restrictionsByte, err := util.Read(r, 1)
if err != nil {
return fmt.Errorf("could not read from reader: %s", err)
}
// a `lazy` approach :), just for now
extended.Flags.Restrictions = restrictionsByte[0]
}
}
// extracting other version-dependent header data
// padding if V2_3
if h.Version == V2_3 {
paddingSizeBytes, err := util.Read(r, 4)
if err != nil {
return fmt.Errorf("could not read from reader: %s", err)
}
paddingSize := util.BytesToInt(paddingSizeBytes)
extended.PaddingSize = paddingSize
}
// finally `attaching` parsed extended header to the main *Header
h.ExtendedHeader = extended
return nil
} }
// Reads and structuralises ID3v2 header from given bytes. // Reads and structuralises ID3v2 header and if present - extended header.
// Returns a blank header struct if encountered an error // Returns a blank header struct if encountered an error
func readHeader(rs io.ReadSeeker) (Header, error) { func readHeader(rs io.ReadSeeker) (Header, error) {
_, err := rs.Seek(0, io.SeekStart) _, err := rs.Seek(0, io.SeekStart)
@ -49,14 +202,8 @@ func readHeader(rs io.ReadSeeker) (Header, error) {
header.Identifier = string(identifier) header.Identifier = string(identifier)
// version // version
majorVersion, err := util.ByteToInt(hBytes[3]) majorVersion := int(hBytes[3])
if err != nil { revisionNumber := int(hBytes[4])
return Header{}, err
}
revisionNumber, err := util.ByteToInt(hBytes[4])
if err != nil {
return Header{}, err
}
switch majorVersion { switch majorVersion {
case 2: case 2:
@ -76,6 +223,17 @@ func readHeader(rs io.ReadSeeker) (Header, error) {
// v3.0 and v4.0 have different amount of flags // v3.0 and v4.0 have different amount of flags
switch header.Version { switch header.Version {
case V2_2:
if flagBits[0] == 1 {
header.Flags.Unsynchronisated = true
} else {
header.Flags.Unsynchronisated = false
}
if flagBits[1] == 1 {
header.Flags.Compressed = true
} else {
header.Flags.Compressed = false
}
case V2_3: case V2_3:
if flagBits[0] == 1 { if flagBits[0] == 1 {
header.Flags.Unsynchronisated = true header.Flags.Unsynchronisated = true
@ -121,9 +279,16 @@ func readHeader(rs io.ReadSeeker) (Header, error) {
// size // size
sizeBytes := hBytes[6:] sizeBytes := hBytes[6:]
size := util.BytesToIntIgnoreFirstBit(sizeBytes) size := util.BytesToIntSynchsafe(sizeBytes)
header.Size = size header.Size = size
if header.Flags.HasExtendedHeader {
err = header.readExtendedHeader(rs)
if err != nil {
return header, err
}
}
return header, nil return header, nil
} }

Loading…
Cancel
Save