id3 seems to be working

id3
Akhil Gupta 4 years ago
parent 9363cc1f0e
commit 727f9b82b9

@ -15,6 +15,7 @@ require (
go.uber.org/zap v1.16.0
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
golang.org/x/text v0.3.2
gorm.io/driver/sqlite v1.1.3
gorm.io/gorm v1.20.2
)

@ -129,6 +129,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20u
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=

@ -0,0 +1,22 @@
(The MIT License)
Copyright (c) 2013 Michael Yang <mikkyangg@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,85 @@
# id3
[![build status](https://travis-ci.org/mikkyang/id3-go.svg)](https://travis-ci.org/mikkyang/id3-go)
ID3 library for Go.
Supported formats:
* ID3v1
* ID3v2.2
* ID3v2.3
# Install
The platform ($GOROOT/bin) "go get" tool is the best method to install.
go get github.com/mikkyang/id3-go
This downloads and installs the package into your $GOPATH. If you only want to
recompile, use "go install".
go install github.com/mikkyang/id3-go
# Usage
An import allows access to the package.
import (
id3 "github.com/mikkyang/id3-go"
)
Version specific details can be accessed through the subpackages.
import (
"github.com/mikkyang/id3-go/v1"
"github.com/mikkyang/id3-go/v2"
)
# Quick Start
To access the tag of a file, first open the file using the package's `Open`
function.
mp3File, err := id3.Open("All-In.mp3")
It's also a good idea to ensure that the file is closed using `defer`.
defer mp3File.Close()
## Accessing Information
Some commonly used data have methods in the tag for easier access. These
methods are for `Title`, `Artist`, `Album`, `Year`, `Genre`, and `Comments`.
mp3File.SetArtist("Okasian")
fmt.Println(mp3File.Artist())
# ID3v2 Frames
v2 Frames can be accessed directly by using the `Frame` or `Frames` method
of the file, which return the first frame or a slice of frames as `Framer`
interfaces. These interfaces allow read access to general details of the file.
lyricsFrame := mp3File.Frame("USLT")
lyrics := lyricsFrame.String()
If more specific information is needed, or frame-specific write access is
needed, then the interface must be cast into the appropriate underlying type.
The example provided does not check for errors, but it is recommended to do
so.
lyricsFrame := mp3File.Frame("USLT").(*v2.UnsynchTextFrame)
## Adding Frames
For common fields, a frame will automatically be created with the `Set` method.
For other frames or more fine-grained control, frames can be created with the
corresponding constructor, usually prefixed by `New`. These constructors require
the first argument to be a FrameType struct, which are global variables named by
version.
ft := V23FrameTypeMap["TIT2"]
text := "Hello"
textFrame := NewTextFrame(ft, text)
mp3File.AddFrames(textFrame)

@ -0,0 +1,87 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package encodedbytes
import (
"errors"
"io"
)
// Reader is a helper for Reading frame bytes
type Reader struct {
data []byte
index int // current Reading index
}
func (r *Reader) Read(b []byte) (n int, err error) {
if len(b) == 0 {
return 0, nil
}
if r.index >= len(r.data) {
return 0, io.EOF
}
n = copy(b, r.data[r.index:])
r.index += n
return
}
func (r *Reader) ReadByte() (b byte, err error) {
if r.index >= len(r.data) {
return 0, io.EOF
}
b = r.data[r.index]
r.index++
return
}
func (r *Reader) ReadNumBytes(n int) ([]byte, error) {
if n <= 0 {
return []byte{}, nil
}
if r.index+n > len(r.data) {
return []byte{}, io.EOF
}
b := make([]byte, n)
_, err := r.Read(b)
return b, err
}
// Read a number of bytes and cast to a string
func (r *Reader) ReadNumBytesString(n int) (string, error) {
b, err := r.ReadNumBytes(n)
return string(b), err
}
// Read until the end of the data
func (r *Reader) ReadRest() ([]byte, error) {
return r.ReadNumBytes(len(r.data) - r.index)
}
// Read until the end of the data and cast to a string
func (r *Reader) ReadRestString(encoding byte) (string, error) {
b, err := r.ReadRest()
if err != nil {
return "", err
}
return Decoders[encoding].String(string(b))
}
// Read a null terminated string of specified encoding
func (r *Reader) ReadNullTermString(encoding byte) (string, error) {
atIndex, afterIndex := nullIndex(r.data[r.index:], encoding)
b, err := r.ReadNumBytes(afterIndex)
if err != nil {
return "", err
}
if -1 == atIndex {
return "", errors.New("could not read null terminated string")
}
return Decoders[encoding].String(string(b[:atIndex]))
}
func NewReader(b []byte) *Reader { return &Reader{b, 0} }

@ -0,0 +1,177 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package encodedbytes
import (
"bytes"
"errors"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/encoding/unicode"
)
const (
BytesPerInt = 4
SynchByteLength = 7
NormByteLength = 8
NativeEncoding = 3
)
type Encoding struct {
Name string
NullLength int
}
var (
EncodingMap = [...]Encoding{
{Name: "ISO-8859-1", NullLength: 1},
{Name: "UTF-16", NullLength: 2},
{Name: "UTF-16BE", NullLength: 2},
{Name: "UTF-8", NullLength: 1},
}
//Decoders = make([]*iconv.Converter, len(EncodingMap))
// //Encoders = make([]*iconv.Converter, len(EncodingMap))
Decoders = make([]*encoding.Decoder, len(EncodingMap))
Encoders = make([]*encoding.Encoder, len(EncodingMap))
)
//func init() {
// n := EncodingForIndex(NativeEncoding)
// for i, e := range EncodingMap {
// d := charmap.ISO8859_1.NewDecoder()
// Decoders[i], _ = iconv.NewConverter(e.Name, n)
// Encoders[i], _ = iconv.NewConverter(n, e.Name)
// }
//}
func init() {
Decoders[0] = charmap.ISO8859_1.NewDecoder()
Decoders[1] = unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()
Decoders[2] = unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder()
Decoders[3] = unicode.UTF8.NewDecoder()
Encoders[0] = charmap.ISO8859_1.NewEncoder()
Encoders[1] = unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewEncoder()
Encoders[2] = unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewEncoder()
Encoders[3] = unicode.UTF8.NewEncoder()
}
// Form an integer from concatenated bits
func ByteInt(buf []byte, base uint) (i uint32, err error) {
if len(buf) > BytesPerInt {
err = errors.New("byte integer: invalid []byte length")
return
}
for _, b := range buf {
if base < NormByteLength && b >= (1<<base) {
err = errors.New("byte integer: exceed max bit")
return
}
i = (i << base) | uint32(b)
}
return
}
func SynchInt(buf []byte) (i uint32, err error) {
i, err = ByteInt(buf, SynchByteLength)
return
}
func NormInt(buf []byte) (i uint32, err error) {
i, err = ByteInt(buf, NormByteLength)
return
}
// Form a byte slice from an integer
func IntBytes(n uint32, base uint) []byte {
mask := uint32(1<<base - 1)
bytes := make([]byte, BytesPerInt)
for i, _ := range bytes {
bytes[len(bytes)-i-1] = byte(n & mask)
n >>= base
}
return bytes
}
func SynchBytes(n uint32) []byte {
return IntBytes(n, SynchByteLength)
}
func NormBytes(n uint32) []byte {
return IntBytes(n, NormByteLength)
}
func EncodingForIndex(b byte) string {
encodingIndex := int(b)
if encodingIndex < 0 || encodingIndex > len(EncodingMap) {
encodingIndex = 0
}
return EncodingMap[encodingIndex].Name
}
func EncodingNullLengthForIndex(b byte) int {
encodingIndex := int(b)
if encodingIndex < 0 || encodingIndex > len(EncodingMap) {
encodingIndex = 0
}
return EncodingMap[encodingIndex].NullLength
}
func IndexForEncoding(e string) byte {
for i, v := range EncodingMap {
if v.Name == e {
return byte(i)
}
}
return 0
}
func nullIndex(data []byte, encoding byte) (atIndex, afterIndex int) {
byteCount := EncodingNullLengthForIndex(encoding)
limit := len(data)
null := bytes.Repeat([]byte{0x0}, byteCount)
for i, _ := range data[:limit/byteCount] {
atIndex = byteCount * i
afterIndex = atIndex + byteCount
if bytes.Equal(data[atIndex:afterIndex], null) {
return
}
}
atIndex = -1
afterIndex = -1
return
}
func EncodedDiff(newEncoding byte, newString string, oldEncoding byte, oldString string) (int, error) {
//newEncodedString, err := Encoders[newEncoding].ConvertString(newString)
//if err != nil {
// return 0, err
//}
//
//oldEncodedString, err := Encoders[oldEncoding].ConvertString(oldString)
//if err != nil {
// return 0, err
//}
newEncodedString, err := Encoders[newEncoding].String(newString)
if err != nil {
return 0, err
}
oldEncodedString, err := Encoders[oldEncoding].String(oldString)
if err != nil {
return 0, err
}
return len(newEncodedString) - len(oldEncodedString), nil
}

@ -0,0 +1,33 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package encodedbytes
import (
"bytes"
"testing"
)
func TestSynch(t *testing.T) {
synch := []byte{0x44, 0x7a, 0x70, 0x04}
const synchResult = 144619524
if result, err := SynchInt(synch); result != synchResult {
t.Errorf("encodedbytes.SynchInt(%v) = %d with error %v, want %d", synch, result, err, synchResult)
}
if result := SynchBytes(synchResult); !bytes.Equal(result, synch) {
t.Errorf("encodedbytes.SynchBytes(%d) = %v, want %v", synchResult, result, synch)
}
}
func TestNorm(t *testing.T) {
norm := []byte{0x0b, 0x95, 0xae, 0xb4}
const normResult = 194358964
if result, err := NormInt(norm); result != normResult {
t.Errorf("encodedbytes.NormInt(%v) = %d with error %v, want %d", norm, result, err, normResult)
}
if result := NormBytes(normResult); !bytes.Equal(result, norm) {
t.Errorf("encodedbytes.NormBytes(%d) = %v, want %v", normResult, result, norm)
}
}

@ -0,0 +1,63 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package encodedbytes
import (
"bytes"
"io"
)
// Writer is a helper for writing frame bytes
type Writer struct {
data []byte
index int // current writing index
}
func (w *Writer) Write(b []byte) (n int, err error) {
if len(b) == 0 {
return 0, nil
}
if w.index >= len(w.data) {
return 0, io.EOF
}
n = copy(w.data[w.index:], b)
w.index += n
return
}
func (w *Writer) WriteByte(b byte) (err error) {
if w.index >= len(w.data) {
return io.EOF
}
w.data[w.index] = b
w.index++
return
}
func (w *Writer) WriteString(s string, encoding byte) (err error) {
encodedString, err := Encoders[encoding].String(s)
if err != nil {
return err
}
_, err = w.Write([]byte(encodedString))
if err != nil {
return err
}
return
}
func (w *Writer) WriteNullTermString(s string, encoding byte) (err error) {
if err = w.WriteString(s, encoding); err != nil {
return
}
nullLength := EncodingNullLengthForIndex(encoding)
_, err = w.Write(bytes.Repeat([]byte{0x0}, nullLength))
return
}
func NewWriter(b []byte) *Writer { return &Writer{b, 0} }

@ -0,0 +1,124 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package id3
import (
"errors"
"os"
v1 "github.com/akhilrex/podgrab/internal/id3/v1"
v2 "github.com/akhilrex/podgrab/internal/id3/v2"
)
const (
LatestVersion = 3
)
// Tagger represents the metadata of a tag
type Tagger interface {
Title() string
Artist() string
Album() string
Year() string
Genre() string
Comments() []string
SetTitle(string)
SetArtist(string)
SetAlbum(string)
SetYear(string)
SetComment(string)
SetGenre(string)
SetDate(string) // Added
SetReleaseYear(string) // Added
AllFrames() []v2.Framer
Frames(string) []v2.Framer
Frame(string) v2.Framer
DeleteFrames(string) []v2.Framer
AddFrames(...v2.Framer)
Bytes() []byte
Dirty() bool
Padding() uint
Size() int
Version() string
}
// File represents the tagged file
type File struct {
Tagger
originalSize int
file *os.File
}
// Parses an open file
func Parse(file *os.File, forcev2 bool) (*File, error) {
res := &File{file: file}
if forcev2 {
res.Tagger = v2.NewTag(2)
} else {
if v2Tag := v2.ParseTag(file); v2Tag != nil {
res.Tagger = v2Tag
res.originalSize = v2Tag.Size()
} else if v1Tag := v1.ParseTag(file); v1Tag != nil {
res.Tagger = v1Tag
} else {
// Add a new tag if none exists
res.Tagger = v2.NewTag(LatestVersion)
}
}
return res, nil
}
// Opens a new tagged file
func Open(name string, forceV2 bool) (*File, error) {
fi, err := os.OpenFile(name, os.O_RDWR, 0666)
if err != nil {
return nil, err
}
file, err := Parse(fi, forceV2)
if err != nil {
return nil, err
}
return file, nil
}
// Saves any edits to the tagged file
func (f *File) Close() error {
defer f.file.Close()
if !f.Dirty() {
return nil
}
switch f.Tagger.(type) {
case (*v1.Tag):
if _, err := f.file.Seek(-v1.TagSize, os.SEEK_END); err != nil {
return err
}
case (*v2.Tag):
if f.Size() > f.originalSize {
start := int64(f.originalSize + v2.HeaderSize)
offset := int64(f.Tagger.Size() - f.originalSize)
if err := shiftBytesBack(f.file, start, offset); err != nil {
return err
}
}
if _, err := f.file.Seek(0, os.SEEK_SET); err != nil {
return err
}
default:
return errors.New("Close: unknown tag version")
}
if _, err := f.file.Write(f.Tagger.Bytes()); err != nil {
return err
}
return nil
}

@ -0,0 +1,278 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package id3
import (
"bytes"
"io"
"io/ioutil"
"os"
"testing"
v2 "github.com/akhilrex/podgrab/internal/id3/v2"
)
const (
testFile = "test.mp3"
)
func TestParse(t *testing.T) {
file, err := os.OpenFile("test.mp3", os.O_RDWR, 0666)
if err != nil {
t.Errorf("Parse: unable to open file")
}
tagger, err := Parse(file)
if err != nil {
t.Errorf("Parse: could not parse")
}
tag, ok := tagger.Tagger.(*v2.Tag)
if !ok {
t.Errorf("Parse: incorrect tagger type")
}
if s := tag.Artist(); s != "Paloalto\x00" {
t.Errorf("Parse: incorrect artist, %v", s)
}
if s := tag.Title(); s != "Nice Life (Feat. Basick)" {
t.Errorf("Parse: incorrect title, %v", s)
}
if s := tag.Album(); s != "Chief Life" {
t.Errorf("Parse: incorrect album, %v", s)
}
parsedFrame := tagger.Frame("COMM")
resultFrame, ok := parsedFrame.(*v2.UnsynchTextFrame)
if !ok {
t.Error("Couldn't cast frame")
}
expected := "✓"
actual := resultFrame.Description()
if expected != actual {
t.Errorf("Expected %q, got %q", expected, actual)
}
actual = resultFrame.Text()
if expected != actual {
t.Errorf("Expected %q, got %q", expected, actual)
}
}
func TestOpen(t *testing.T) {
file, err := Open(testFile)
if err != nil {
t.Errorf("Open: unable to open file")
}
tag, ok := file.Tagger.(*v2.Tag)
if !ok {
t.Errorf("Open: incorrect tagger type")
}
if s := tag.Artist(); s != "Paloalto\x00" {
t.Errorf("Open: incorrect artist, %v", s)
}
if s := tag.Title(); s != "Nice Life (Feat. Basick)" {
t.Errorf("Open: incorrect title, %v", s)
}
if s := tag.Album(); s != "Chief Life" {
t.Errorf("Open: incorrect album, %v", s)
}
parsedFrame := file.Frame("COMM")
resultFrame, ok := parsedFrame.(*v2.UnsynchTextFrame)
if !ok {
t.Error("Couldn't cast frame")
}
expected := "✓"
actual := resultFrame.Description()
if expected != actual {
t.Errorf("Expected %q, got %q", expected, actual)
}
actual = resultFrame.Text()
if expected != actual {
t.Errorf("Expected %q, got %q", expected, actual)
}
}
func TestClose(t *testing.T) {
before, err := ioutil.ReadFile(testFile)
if err != nil {
t.Errorf("test file error")
}
file, err := Open(testFile)
if err != nil {
t.Errorf("Close: unable to open file")
}
beforeCutoff := file.originalSize
file.SetArtist("Paloalto")
file.SetTitle("Test test test test test test")
afterCutoff := file.Size()
if err := file.Close(); err != nil {
t.Errorf("Close: unable to close file")
}
after, err := ioutil.ReadFile(testFile)
if err != nil {
t.Errorf("Close: unable to reopen file")
}
if !bytes.Equal(before[beforeCutoff:], after[afterCutoff:]) {
t.Errorf("Close: nontag data lost on close")
}
if err := ioutil.WriteFile(testFile, before, 0666); err != nil {
t.Errorf("Close: unable to write original contents to test file")
}
}
func TestReadonly(t *testing.T) {
before, err := ioutil.ReadFile(testFile)
if err != nil {
t.Errorf("test file error")
}
file, err := Open(testFile)
if err != nil {
t.Errorf("Readonly: unable to open file")
}
file.Title()
file.Artist()
file.Album()
file.Year()
file.Genre()
file.Comments()
if err := file.Close(); err != nil {
t.Errorf("Readonly: unable to close file")
}
after, err := ioutil.ReadFile(testFile)
if err != nil {
t.Errorf("Readonly: unable to reopen file")
}
if !bytes.Equal(before, after) {
t.Errorf("Readonly: tag data modified without set")
}
}
func TestAddTag(t *testing.T) {
tempFile, err := ioutil.TempFile("", "notag")
if err != nil {
t.Fatal(err)
}
file, err := Open(tempFile.Name())
if err != nil {
t.Errorf("AddTag: unable to open empty file")
}
tag := file.Tagger
if tag == nil {
t.Errorf("AddTag: no tag added to file")
}
file.SetArtist("Michael")
err = file.Close()
if err != nil {
t.Errorf("AddTag: error closing new file")
}
reopenBytes, err := ioutil.ReadFile(tempFile.Name())
if err != nil {
t.Errorf("AddTag: error reopening file")
}
expectedBytes := tag.Bytes()
if !bytes.Equal(expectedBytes, reopenBytes) {
t.Errorf("AddTag: tag not written correctly: %v", reopenBytes)
}
}
func TestUnsynchTextFrame_RoundTrip(t *testing.T) {
var (
err error
tempfile *os.File
f *File
tagger *v2.Tag
ft v2.FrameType
utextFrame *v2.UnsynchTextFrame
parsedFrame v2.Framer
resultFrame *v2.UnsynchTextFrame
ok bool
expected, actual string
)
tempfile, err = ioutil.TempFile("", "id3v2")
if err != nil {
t.Fatal(err)
}
tagger = v2.NewTag(3)
ft = v2.V23FrameTypeMap["COMM"]
utextFrame = v2.NewUnsynchTextFrame(ft, "Comment", "Foo")
tagger.AddFrames(utextFrame)
_, err = tempfile.Write(tagger.Bytes())
tempfile.Close()
if err != nil {
t.Fatal(err)
}
f, err = Open(tempfile.Name())
if err != nil {
t.Fatal(err)
}
parsedFrame = f.Frame("COMM")
if resultFrame, ok = parsedFrame.(*v2.UnsynchTextFrame); !ok {
t.Error("Couldn't cast frame")
} else {
expected = utextFrame.Description()
actual = resultFrame.Description()
if expected != actual {
t.Errorf("Expected %q, got %q", expected, actual)
}
}
}
func TestUTF16CommPanic(t *testing.T) {
osFile, err := os.Open(testFile)
if err != nil {
t.Error(err)
}
tempfile, err := ioutil.TempFile("", "utf16_comm")
if err != nil {
t.Error(err)
}
io.Copy(tempfile, osFile)
osFile.Close()
tempfile.Close()
for i := 0; i < 2; i++ {
file, err := Open(tempfile.Name())
if err != nil {
t.Error(err)
}
file.Close()
}
}

@ -0,0 +1,59 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package id3
import (
"io"
"os"
)
func shiftBytesBack(file *os.File, start, offset int64) error {
stat, err := file.Stat()
if err != nil {
return err
}
end := stat.Size()
wrBuf := make([]byte, offset)
rdBuf := make([]byte, offset)
wrOffset := offset
rdOffset := start
rn, err := file.ReadAt(wrBuf, rdOffset)
if err != nil && err != io.EOF {
panic(err)
}
rdOffset += int64(rn)
for {
if rdOffset >= end {
break
}
n, err := file.ReadAt(rdBuf, rdOffset)
if err != nil && err != io.EOF {
return err
}
if rdOffset+int64(n) > end {
n = int(end - rdOffset)
}
if _, err := file.WriteAt(wrBuf[:rn], wrOffset); err != nil {
return err
}
rdOffset += int64(n)
wrOffset += int64(rn)
copy(wrBuf, rdBuf)
rn = n
}
if _, err := file.WriteAt(wrBuf[:rn], wrOffset); err != nil {
return err
}
return nil
}

@ -0,0 +1,169 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v1
import (
"io"
"os"
v2 "github.com/akhilrex/podgrab/internal/id3/v2"
)
const (
TagSize = 128
)
var (
Genres = []string{
"Blues", "Classic Rock", "Country", "Dance",
"Disco", "Funk", "Grunge", "Hip-Hop",
"Jazz", "Metal", "New Age", "Oldies",
"Other", "Pop", "R&B", "Rap",
"Reggae", "Rock", "Techno", "Industrial",
"Alternative", "Ska", "Death Metal", "Pranks",
"Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop",
"Vocal", "Jazz+Funk", "Fusion", "Trance",
"Classical", "Instrumental", "Acid", "House",
"Game", "Sound Clip", "Gospel", "Noise",
"AlternRock", "Bass", "Soul", "Punk",
"Space", "Meditative", "Instrumental Pop", "Instrumental Rock",
"Ethnic", "Gothic", "Darkwave", "Techno-Industrial",
"Electronic", "Pop-Folk", "Eurodance", "Dream",
"Southern Rock", "Comedy", "Cult", "Gangsta",
"Top 40", "Christian Rap", "Pop/Funk", "Jungle",
"Native American", "Cabaret", "New Wave", "Psychadelic",
"Rave", "Showtunes", "Trailer", "Lo-Fi",
"Tribal", "Acid Punk", "Acid Jazz", "Polka",
"Retro", "Musical", "Rock & Roll", "Hard Rock",
}
)
// Tag represents an ID3v1 tag
type Tag struct {
title, artist, album, year, comment string
genre byte
dirty bool
}
func ParseTag(readSeeker io.ReadSeeker) *Tag {
readSeeker.Seek(-TagSize, os.SEEK_END)
data := make([]byte, TagSize)
n, err := io.ReadFull(readSeeker, data)
if n < TagSize || err != nil || string(data[:3]) != "TAG" {
return nil
}
return &Tag{
title: string(data[3:33]),
artist: string(data[33:63]),
album: string(data[63:93]),
year: string(data[93:97]),
comment: string(data[97:127]),
genre: data[127],
dirty: false,
}
}
func (t Tag) Dirty() bool {
return t.dirty
}
func (t Tag) Title() string { return t.title }
func (t Tag) Artist() string { return t.artist }
func (t Tag) Album() string { return t.album }
func (t Tag) Year() string { return t.year }
func (t Tag) Genre() string {
if int(t.genre) < len(Genres) {
return Genres[t.genre]
}
return ""
}
func (t Tag) Comments() []string {
return []string{t.comment}
}
func (t *Tag) SetTitle(text string) {
t.title = text
t.dirty = true
}
func (t *Tag) SetArtist(text string) {
t.artist = text
t.dirty = true
}
func (t *Tag) SetAlbum(text string) {
t.album = text
t.dirty = true
}
func (t *Tag) SetYear(text string) {
t.year = text
t.dirty = true
}
func (t *Tag) SetComment(text string) {
length := 28
if len(text) < length {
length = len((text))
}
t.comment = text[0:length]
t.dirty = true
}
func (t *Tag) SetDate(text string) {
t.year = text
t.dirty = true
print("SetDate() not implemented for ID3v1 tags")
}
func (t *Tag) SetReleaseYear(text string) {
t.year = text
t.dirty = true
print("SetReleaseYear() not implemented for ID3v1 tags")
}
func (t *Tag) SetGenre(text string) {
t.genre = 255
for i, genre := range Genres {
if text == genre {
t.genre = byte(i)
break
}
}
t.dirty = true
}
func (t Tag) Bytes() []byte {
data := make([]byte, TagSize)
copy(data[:3], []byte("TAG"))
copy(data[3:33], []byte(t.title))
copy(data[33:63], []byte(t.artist))
copy(data[63:93], []byte(t.album))
copy(data[93:97], []byte(t.year))
copy(data[97:127], []byte(t.comment))
data[127] = t.genre
return data
}
func (t Tag) Size() int {
return TagSize
}
func (t Tag) Version() string {
return "1.0"
}
// Dummy methods to satisfy Tagger interface
func (t Tag) Padding() uint { return 0 }
func (t Tag) AllFrames() []v2.Framer { return []v2.Framer{} }
func (t Tag) Frame(id string) v2.Framer { return nil }
func (t Tag) Frames(id string) []v2.Framer { return []v2.Framer{} }
func (t Tag) DeleteFrames(id string) []v2.Framer { return []v2.Framer{} }
func (t Tag) AddFrames(f ...v2.Framer) {}

@ -0,0 +1,586 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v2
import (
"errors"
"fmt"
"github.com/akhilrex/podgrab/internal/id3/encodedbytes"
)
const (
FrameHeaderSize = 10
)
// FrameType holds frame id metadata and constructor method
// A set number of these are created in the version specific files
type FrameType struct {
id string
description string
constructor func(FrameHead, []byte) Framer
}
// Framer provides a generic interface for frames
// This is the default type returned when creating frames
type Framer interface {
Id() string
Size() uint
StatusFlags() byte
FormatFlags() byte
String() string
Bytes() []byte
setOwner(*Tag)
}
// FrameHead represents the header of each frame
// Additional metadata is kept through the embedded frame type
// These do not usually need to be manually created
type FrameHead struct {
FrameType
statusFlags byte
formatFlags byte
size uint32
owner *Tag
}
func (ft FrameType) Id() string {
return ft.id
}
func (h FrameHead) Size() uint {
return uint(h.size)
}
func (h *FrameHead) changeSize(diff int) {
if diff >= 0 {
h.size += uint32(diff)
} else {
h.size -= uint32(-diff)
}
if h.owner != nil {
h.owner.changeSize(diff)
}
}
func (h FrameHead) StatusFlags() byte {
return h.statusFlags
}
func (h FrameHead) FormatFlags() byte {
return h.formatFlags
}
func (h *FrameHead) setOwner(t *Tag) {
h.owner = t
}
// DataFrame is the default frame for binary data
type DataFrame struct {
FrameHead
data []byte
}
func NewDataFrame(ft FrameType, data []byte) *DataFrame {
head := FrameHead{
FrameType: ft,
size: uint32(len(data)),
}
return &DataFrame{head, data}
}
func ParseDataFrame(head FrameHead, data []byte) Framer {
return &DataFrame{head, data}
}
func (f DataFrame) Data() []byte {
return f.data
}
func (f *DataFrame) SetData(b []byte) {
diff := len(b) - len(f.data)
f.changeSize(diff)
f.data = b
}
func (f DataFrame) String() string {
return "<binary data>"
}
func (f DataFrame) Bytes() []byte {
return f.data
}
// IdFrame represents identification tags
type IdFrame struct {
FrameHead
ownerIdentifier string
identifier []byte
}
func NewIdFrame(ft FrameType, ownerId string, id []byte) *IdFrame {
head := FrameHead{
FrameType: ft,
size: uint32(1 + len(ownerId) + len(id)),
}
return &IdFrame{
FrameHead: head,
ownerIdentifier: ownerId,
identifier: id,
}
}
func ParseIdFrame(head FrameHead, data []byte) Framer {
var err error
f := &IdFrame{FrameHead: head}
rd := encodedbytes.NewReader(data)
if f.ownerIdentifier, err = rd.ReadNullTermString(encodedbytes.NativeEncoding); err != nil {
return nil
}
if f.identifier, err = rd.ReadRest(); len(f.identifier) > 64 || err != nil {
return nil
}
return f
}
func (f IdFrame) OwnerIdentifier() string {
return f.ownerIdentifier
}
func (f *IdFrame) SetOwnerIdentifier(ownerId string) {
f.changeSize(len(ownerId) - len(f.ownerIdentifier))
f.ownerIdentifier = ownerId
}
func (f IdFrame) Identifier() []byte {
return f.identifier
}
func (f *IdFrame) SetIdentifier(id []byte) error {
if len(id) > 64 {
return errors.New("identifier: identifier too long")
}
f.changeSize(len(id) - len(f.identifier))
f.identifier = id
return nil
}
func (f IdFrame) String() string {
return fmt.Sprintf("%s: %v", f.ownerIdentifier, f.identifier)
}
func (f IdFrame) Bytes() []byte {
var err error
bytes := make([]byte, f.Size())
wr := encodedbytes.NewWriter(bytes)
if err = wr.WriteString(f.ownerIdentifier, encodedbytes.NativeEncoding); err != nil {
return bytes
}
if _, err = wr.Write(f.identifier); err != nil {
return bytes
}
return bytes
}
// TextFramer represents frames that contain encoded text
type TextFramer interface {
Framer
Encoding() string
SetEncoding(string) error
Text() string
SetText(string) error
}
// TextFrame represents frames that contain encoded text
type TextFrame struct {
FrameHead
encoding byte
text string
}
func NewTextFrame(ft FrameType, text string) *TextFrame {
head := FrameHead{
FrameType: ft,
size: uint32(1 + len(text)),
}
return &TextFrame{
FrameHead: head,
text: text,
}
}
func ParseTextFrame(head FrameHead, data []byte) Framer {
var err error
f := &TextFrame{FrameHead: head}
rd := encodedbytes.NewReader(data)
if f.encoding, err = rd.ReadByte(); err != nil {
return nil
}
if f.text, err = rd.ReadRestString(f.encoding); err != nil {
return nil
}
return f
}
func (f TextFrame) Encoding() string {
return encodedbytes.EncodingForIndex(f.encoding)
}
func (f *TextFrame) SetEncoding(encoding string) error {
i := byte(encodedbytes.IndexForEncoding(encoding))
if i < 0 {
return errors.New("encoding: invalid encoding")
}
diff, err := encodedbytes.EncodedDiff(i, f.text, f.encoding, f.text)
if err != nil {
return err
}
f.changeSize(diff)
f.encoding = i
return nil
}
func (f TextFrame) Text() string {
return f.text
}
func (f *TextFrame) SetText(text string) error {
diff, err := encodedbytes.EncodedDiff(f.encoding, text, f.encoding, f.text)
if err != nil {
return err
}
f.changeSize(diff)
f.text = text
return nil
}
func (f TextFrame) String() string {
return f.text
}
func (f TextFrame) Bytes() []byte {
var err error
bytes := make([]byte, f.Size())
wr := encodedbytes.NewWriter(bytes)
if err = wr.WriteByte(f.encoding); err != nil {
return bytes
}
if err = wr.WriteString(f.text, f.encoding); err != nil {
return bytes
}
return bytes
}
type DescTextFrame struct {
TextFrame
description string
}
func NewDescTextFrame(ft FrameType, desc, text string) *DescTextFrame {
f := NewTextFrame(ft, text)
nullLength := encodedbytes.EncodingNullLengthForIndex(f.encoding)
f.size += uint32(len(desc) + nullLength)
return &DescTextFrame{
TextFrame: *f,
description: desc,
}
}
// DescTextFrame represents frames that contain encoded text and descriptions
func ParseDescTextFrame(head FrameHead, data []byte) Framer {
var err error
f := new(DescTextFrame)
f.FrameHead = head
rd := encodedbytes.NewReader(data)
if f.encoding, err = rd.ReadByte(); err != nil {
return nil
}
if f.description, err = rd.ReadNullTermString(f.encoding); err != nil {
return nil
}
if f.text, err = rd.ReadRestString(f.encoding); err != nil {
return nil
}
return f
}
func (f DescTextFrame) Description() string {
return f.description
}
func (f *DescTextFrame) SetDescription(description string) error {
diff, err := encodedbytes.EncodedDiff(f.encoding, description, f.encoding, f.description)
if err != nil {
return err
}
f.changeSize(diff)
f.description = description
return nil
}
func (f *DescTextFrame) SetEncoding(encoding string) error {
i := byte(encodedbytes.IndexForEncoding(encoding))
if i < 0 {
return errors.New("encoding: invalid encoding")
}
descDiff, err := encodedbytes.EncodedDiff(i, f.text, f.encoding, f.text)
if err != nil {
return err
}
newNullLength := encodedbytes.EncodingNullLengthForIndex(i)
oldNullLength := encodedbytes.EncodingNullLengthForIndex(f.encoding)
nullDiff := newNullLength - oldNullLength
textDiff, err := encodedbytes.EncodedDiff(i, f.description, f.encoding, f.description)
if err != nil {
return err
}
f.changeSize(descDiff + nullDiff + textDiff)
f.encoding = i
return nil
}
func (f DescTextFrame) String() string {
return fmt.Sprintf("%s: %s", f.description, f.text)
}
func (f DescTextFrame) Bytes() []byte {
var err error
bytes := make([]byte, f.Size())
wr := encodedbytes.NewWriter(bytes)
if err = wr.WriteByte(f.encoding); err != nil {
return bytes
}
if err = wr.WriteString(f.description, f.encoding); err != nil {
return bytes
}
if err = wr.WriteString(f.text, f.encoding); err != nil {
return bytes
}
return bytes
}
// UnsynchTextFrame represents frames that contain unsynchronized text
type UnsynchTextFrame struct {
DescTextFrame
language string
}
func NewUnsynchTextFrame(ft FrameType, desc, text string) *UnsynchTextFrame {
f := NewDescTextFrame(ft, desc, text)
f.size += uint32(3)
return &UnsynchTextFrame{
DescTextFrame: *f,
language: "eng",
}
}
func ParseUnsynchTextFrame(head FrameHead, data []byte) Framer {
var err error
f := new(UnsynchTextFrame)
f.FrameHead = head
rd := encodedbytes.NewReader(data)
if f.encoding, err = rd.ReadByte(); err != nil {
return nil
}
if f.language, err = rd.ReadNumBytesString(3); err != nil {
return nil
}
if f.description, err = rd.ReadNullTermString(f.encoding); err != nil {
return nil
}
if f.text, err = rd.ReadRestString(f.encoding); err != nil {
return nil
}
return f
}
func (f UnsynchTextFrame) Language() string {
return f.language
}
func (f *UnsynchTextFrame) SetLanguage(language string) error {
if len(language) != 3 {
return errors.New("language: invalid language string")
}
f.language = language
f.changeSize(0)
return nil
}
func (f UnsynchTextFrame) String() string {
return fmt.Sprintf("%s\t%s:\n%s", f.language, f.description, f.text)
}
func (f UnsynchTextFrame) Bytes() []byte {
var err error
bytes := make([]byte, f.Size())
wr := encodedbytes.NewWriter(bytes)
if err = wr.WriteByte(f.encoding); err != nil {
return bytes
}
if err = wr.WriteString(f.language, encodedbytes.NativeEncoding); err != nil {
return bytes
}
if err = wr.WriteNullTermString(f.description, f.encoding); err != nil {
return bytes
}
if err = wr.WriteString(f.text, f.encoding); err != nil {
return bytes
}
return bytes
}
// ImageFrame represent frames that have media attached
type ImageFrame struct {
DataFrame
encoding byte
mimeType string
pictureType byte
description string
}
func ParseImageFrame(head FrameHead, data []byte) Framer {
var err error
f := new(ImageFrame)
f.FrameHead = head
rd := encodedbytes.NewReader(data)
if f.encoding, err = rd.ReadByte(); err != nil {
return nil
}
if f.mimeType, err = rd.ReadNullTermString(encodedbytes.NativeEncoding); err != nil {
return nil
}
if f.pictureType, err = rd.ReadByte(); err != nil {
return nil
}
if f.description, err = rd.ReadNullTermString(f.encoding); err != nil {
return nil
}
if f.data, err = rd.ReadRest(); err != nil {
return nil
}
return f
}
func (f ImageFrame) Encoding() string {
return encodedbytes.EncodingForIndex(f.encoding)
}
func (f *ImageFrame) SetEncoding(encoding string) error {
i := byte(encodedbytes.IndexForEncoding(encoding))
if i < 0 {
return errors.New("encoding: invalid encoding")
}
diff, err := encodedbytes.EncodedDiff(i, f.description, f.encoding, f.description)
if err != nil {
return err
}
f.changeSize(diff)
f.encoding = i
return nil
}
func (f ImageFrame) MIMEType() string {
return f.mimeType
}
func (f *ImageFrame) SetMIMEType(mimeType string) {
diff := len(mimeType) - len(f.mimeType)
if mimeType[len(mimeType)-1] != 0 {
nullTermBytes := append([]byte(mimeType), 0x00)
f.mimeType = string(nullTermBytes)
diff += 1
} else {
f.mimeType = mimeType
}
f.changeSize(diff)
}
func (f ImageFrame) String() string {
return fmt.Sprintf("%s\t%s: <binary data>", f.mimeType, f.description)
}
func (f ImageFrame) Bytes() []byte {
var err error
bytes := make([]byte, f.Size())
wr := encodedbytes.NewWriter(bytes)
if err = wr.WriteByte(f.encoding); err != nil {
return bytes
}
if err = wr.WriteString(f.mimeType, encodedbytes.NativeEncoding); err != nil {
return bytes
}
if err = wr.WriteByte(f.pictureType); err != nil {
return bytes
}
if err = wr.WriteString(f.description, f.encoding); err != nil {
return bytes
}
if n, err := wr.Write(f.data); n < len(f.data) || err != nil {
return bytes
}
return bytes
}

@ -0,0 +1,30 @@
package v2
import (
"testing"
)
func TestUnsynchTextFrameSetEncoding(t *testing.T) {
f := NewUnsynchTextFrame(V23CommonFrame["Comments"], "Foo", "Bar")
size := f.Size()
expectedDiff := 11
err := f.SetEncoding("UTF-16")
if err != nil {
t.Fatal(err)
}
newSize := f.Size()
if int(newSize-size) != expectedDiff {
t.Errorf("expected size to increase to %d, but it was %d", size+1, newSize)
}
size = newSize
err = f.SetEncoding("ISO-8859-1")
if err != nil {
t.Fatal(err)
}
newSize = f.Size()
if int(newSize-size) != -expectedDiff {
t.Errorf("expected size to decrease to %d, but it was %d", size-1, newSize)
}
}

@ -0,0 +1,372 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v2
import (
"fmt"
"io"
"os"
"github.com/akhilrex/podgrab/internal/id3/encodedbytes"
)
const (
HeaderSize = 10
)
// Tag represents an ID3v2 tag
type Tag struct {
*Header
frames map[string][]Framer
padding uint
commonMap map[string]FrameType
frameHeaderSize int
frameConstructor func(io.Reader) Framer
frameBytesConstructor func(Framer) []byte
dirty bool
}
// Creates a new tag
func NewTag(version byte) *Tag {
header := &Header{version: version}
t := &Tag{
Header: header,
frames: make(map[string][]Framer),
dirty: false,
}
switch t.version {
case 2:
t.commonMap = V22CommonFrame
t.frameConstructor = ParseV22Frame
t.frameHeaderSize = V22FrameHeaderSize
t.frameBytesConstructor = V22Bytes
case 3:
t.commonMap = V23CommonFrame
t.frameConstructor = ParseV23Frame
t.frameHeaderSize = FrameHeaderSize
t.frameBytesConstructor = V23Bytes
default:
t.commonMap = V23CommonFrame
t.frameConstructor = ParseV23Frame
t.frameHeaderSize = FrameHeaderSize
t.frameBytesConstructor = V23Bytes
}
return t
}
// Parses a new tag
func ParseTag(readSeeker io.ReadSeeker) *Tag {
header := ParseHeader(readSeeker)
if header == nil {
return nil
}
t := NewTag(header.version)
t.Header = header
var frame Framer
size := int(t.size)
for size > 0 {
frame = t.frameConstructor(readSeeker)
if frame == nil {
break
}
id := frame.Id()
t.frames[id] = append(t.frames[id], frame)
frame.setOwner(t)
size -= t.frameHeaderSize + int(frame.Size())
}
t.padding = uint(size)
if _, err := readSeeker.Seek(int64(HeaderSize+t.Size()), os.SEEK_SET); err != nil {
return nil
}
return t
}
// Real size of the tag
func (t Tag) RealSize() int {
size := uint(t.size) - t.padding
return int(size)
}
func (t *Tag) changeSize(diff int) {
if d := int(t.padding) - diff; d < 0 {
t.padding = 0
t.size += uint32(-d)
} else {
t.padding = uint(d)
}
t.dirty = true
}
// Modified status of the tag
func (t Tag) Dirty() bool {
return t.dirty
}
func (t Tag) Bytes() []byte {
data := make([]byte, t.Size())
index := 0
for _, v := range t.frames {
for _, f := range v {
size := t.frameHeaderSize + int(f.Size())
copy(data[index:index+size], t.frameBytesConstructor(f))
index += size
}
}
return append(t.Header.Bytes(), data...)
}
// The amount of padding in the tag
func (t Tag) Padding() uint {
return t.padding
}
// All frames
func (t Tag) AllFrames() []Framer {
// Most of the time each ID will only have one frame
m := len(t.frames)
frames := make([]Framer, m)
i := 0
for _, frameSlice := range t.frames {
if i >= m {
frames = append(frames, frameSlice...)
}
n := copy(frames[i:], frameSlice)
i += n
if n < len(frameSlice) {
frames = append(frames, frameSlice[n:]...)
}
}
return frames
}
// All frames with specified ID
func (t Tag) Frames(id string) []Framer {
if frames, ok := t.frames[id]; ok && frames != nil {
return frames
}
return []Framer{}
}
// First frame with specified ID
func (t Tag) Frame(id string) Framer {
if frames := t.Frames(id); len(frames) != 0 {
return frames[0]
}
return nil
}
// Delete and return all frames with specified ID
func (t *Tag) DeleteFrames(id string) []Framer {
frames := t.Frames(id)
if frames == nil {
return nil
}
diff := 0
for _, frame := range frames {
frame.setOwner(nil)
diff += t.frameHeaderSize + int(frame.Size())
}
t.changeSize(-diff)
delete(t.frames, id)
return frames
}
// Add frames
func (t *Tag) AddFrames(frames ...Framer) {
for _, frame := range frames {
t.changeSize(t.frameHeaderSize + int(frame.Size()))
id := frame.Id()
t.frames[id] = append(t.frames[id], frame)
frame.setOwner(t)
}
}
func (t Tag) Title() string {
return t.textFrameText(t.commonMap["Title"])
}
func (t Tag) Artist() string {
return t.textFrameText(t.commonMap["Artist"])
}
func (t Tag) Album() string {
return t.textFrameText(t.commonMap["Album"])
}
func (t Tag) Year() string {
return t.textFrameText(t.commonMap["Year"])
}
func (t Tag) Genre() string {
return t.textFrameText(t.commonMap["Genre"])
}
func (t Tag) Comments() []string {
frames := t.Frames(t.commonMap["Comments"].Id())
if frames == nil {
return nil
}
comments := make([]string, len(frames))
for i, frame := range frames {
comments[i] = frame.String()
}
return comments
}
func (t *Tag) SetTitle(text string) {
t.setTextFrameText(t.commonMap["Title"], text)
}
func (t *Tag) SetArtist(text string) {
t.setTextFrameText(t.commonMap["Artist"], text)
}
func (t *Tag) SetComment(text string) {
t.setUnsynchTextFrameText(t.commonMap["Comments"], text)
}
func (t *Tag) SetAlbum(text string) {
t.setTextFrameText(t.commonMap["Album"], text)
}
func (t *Tag) SetYear(text string) {
t.setTextFrameText(t.commonMap["Year"], text)
}
func (t *Tag) SetDate(text string) {
t.setTextFrameText(t.commonMap["Date"], text)
}
func (t *Tag) SetReleaseYear(text string) {
t.setTextFrameText(t.commonMap["ReleaseYear"], text)
}
func (t *Tag) SetGenre(text string) {
t.setTextFrameText(t.commonMap["Genre"], text)
}
func (t *Tag) textFrame(ft FrameType) TextFramer {
if frame := t.Frame(ft.Id()); frame != nil {
if textFramer, ok := frame.(TextFramer); ok {
return textFramer
}
}
return nil
}
func (t Tag) textFrameText(ft FrameType) string {
if frame := t.textFrame(ft); frame != nil {
return frame.Text()
}
return ""
}
func (t *Tag) setTextFrameText(ft FrameType, text string) {
if frame := t.textFrame(ft); frame != nil {
frame.SetEncoding("UTF-8")
frame.SetText(text)
} else {
f := NewTextFrame(ft, text)
f.SetEncoding("UTF-8")
t.AddFrames(f)
}
}
func (t *Tag) setUnsynchTextFrameText(ft FrameType, text string) {
if frame := t.textFrame(ft); frame != nil {
frame.SetEncoding("UTF-8")
frame.SetText(text)
} else {
f := NewUnsynchTextFrame(ft, "comments", text)
f.SetEncoding("UTF-8")
t.AddFrames(f)
}
}
func ParseHeader(reader io.Reader) *Header {
data := make([]byte, HeaderSize)
n, err := io.ReadFull(reader, data)
if n < HeaderSize || err != nil || string(data[:3]) != "ID3" {
return nil
}
size, err := encodedbytes.SynchInt(data[6:])
if err != nil {
return nil
}
header := &Header{
version: data[3],
revision: data[4],
flags: data[5],
size: size,
}
switch header.version {
case 2:
header.unsynchronization = isBitSet(header.flags, 7)
header.compression = isBitSet(header.flags, 6)
case 3:
header.unsynchronization = isBitSet(header.flags, 7)
header.extendedHeader = isBitSet(header.flags, 6)
header.experimental = isBitSet(header.flags, 5)
}
return header
}
// Header represents the data of the header of the entire tag
type Header struct {
version, revision byte
flags byte
unsynchronization bool
compression bool
experimental bool
extendedHeader bool
size uint32
}
func (h Header) Version() string {
return fmt.Sprintf("2.%d.%d", h.version, h.revision)
}
func (h Header) Size() int {
return int(h.size)
}
func (h Header) Bytes() []byte {
data := make([]byte, 0, HeaderSize)
data = append(data, "ID3"...)
data = append(data, h.version, h.revision, h.flags)
data = append(data, encodedbytes.SynchBytes(h.size)...)
return data
}

@ -0,0 +1,134 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v2
import (
"io"
"github.com/akhilrex/podgrab/internal/id3/encodedbytes"
)
const (
V22FrameHeaderSize = 6
)
var (
// Common frame IDs
V22CommonFrame = map[string]FrameType{
"Title": V22FrameTypeMap["TT2"],
"Artist": V22FrameTypeMap["TP1"],
"Album": V22FrameTypeMap["TAL"],
"Year": V22FrameTypeMap["TYE"],
"Genre": V22FrameTypeMap["TCO"],
"Comments": V22FrameTypeMap["COM"],
"Date": V22FrameTypeMap["TDA"],
"ReleaseYear": V22FrameTypeMap["TOR"],
}
// V22FrameTypeMap specifies the frame IDs and constructors allowed in ID3v2.2
V22FrameTypeMap = map[string]FrameType{
"BUF": FrameType{id: "BUF", description: "Recommended buffer size", constructor: ParseDataFrame},
"CNT": FrameType{id: "CNT", description: "Play counter", constructor: ParseDataFrame},
"COM": FrameType{id: "COM", description: "Comments", constructor: ParseUnsynchTextFrame},
"CRA": FrameType{id: "CRA", description: "Audio encryption", constructor: ParseDataFrame},
"CRM": FrameType{id: "CRM", description: "Encrypted meta frame", constructor: ParseDataFrame},
"ETC": FrameType{id: "ETC", description: "Event timing codes", constructor: ParseDataFrame},
"EQU": FrameType{id: "EQU", description: "Equalization", constructor: ParseDataFrame},
"GEO": FrameType{id: "GEO", description: "General encapsulated object", constructor: ParseDataFrame},
"IPL": FrameType{id: "IPL", description: "Involved people list", constructor: ParseDataFrame},
"LNK": FrameType{id: "LNK", description: "Linked information", constructor: ParseDataFrame},
"MCI": FrameType{id: "MCI", description: "Music CD Identifier", constructor: ParseDataFrame},
"MLL": FrameType{id: "MLL", description: "MPEG location lookup table", constructor: ParseDataFrame},
"PIC": FrameType{id: "PIC", description: "Attached picture", constructor: ParseDataFrame},
"POP": FrameType{id: "POP", description: "Popularimeter", constructor: ParseDataFrame},
"REV": FrameType{id: "REV", description: "Reverb", constructor: ParseDataFrame},
"RVA": FrameType{id: "RVA", description: "Relative volume adjustment", constructor: ParseDataFrame},
"SLT": FrameType{id: "SLT", description: "Synchronized lyric/text", constructor: ParseDataFrame},
"STC": FrameType{id: "STC", description: "Synced tempo codes", constructor: ParseDataFrame},
"TAL": FrameType{id: "TAL", description: "Album/Movie/Show title", constructor: ParseTextFrame},
"TBP": FrameType{id: "TBP", description: "BPM (Beats Per Minute)", constructor: ParseTextFrame},
"TCM": FrameType{id: "TCM", description: "Composer", constructor: ParseTextFrame},
"TCO": FrameType{id: "TCO", description: "Content type", constructor: ParseTextFrame},
"TCR": FrameType{id: "TCR", description: "Copyright message", constructor: ParseTextFrame},
"TDA": FrameType{id: "TDA", description: "Date", constructor: ParseTextFrame},
"TDY": FrameType{id: "TDY", description: "Playlist delay", constructor: ParseTextFrame},
"TEN": FrameType{id: "TEN", description: "Encoded by", constructor: ParseTextFrame},
"TFT": FrameType{id: "TFT", description: "File type", constructor: ParseTextFrame},
"TIM": FrameType{id: "TIM", description: "Time", constructor: ParseTextFrame},
"TKE": FrameType{id: "TKE", description: "Initial key", constructor: ParseTextFrame},
"TLA": FrameType{id: "TLA", description: "Language(s)", constructor: ParseTextFrame},
"TLE": FrameType{id: "TLE", description: "Length", constructor: ParseTextFrame},
"TMT": FrameType{id: "TMT", description: "Media type", constructor: ParseTextFrame},
"TOA": FrameType{id: "TOA", description: "Original artist(s)/performer(s)", constructor: ParseTextFrame},
"TOF": FrameType{id: "TOF", description: "Original filename", constructor: ParseTextFrame},
"TOL": FrameType{id: "TOL", description: "Original Lyricist(s)/text writer(s)", constructor: ParseTextFrame},
"TOR": FrameType{id: "TOR", description: "Original release year", constructor: ParseTextFrame},
"TOT": FrameType{id: "TOT", description: "Original album/Movie/Show title", constructor: ParseTextFrame},
"TP1": FrameType{id: "TP1", description: "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group", constructor: ParseTextFrame},
"TP2": FrameType{id: "TP2", description: "Band/Orchestra/Accompaniment", constructor: ParseTextFrame},
"TP3": FrameType{id: "TP3", description: "Conductor/Performer refinement", constructor: ParseTextFrame},
"TP4": FrameType{id: "TP4", description: "Interpreted, remixed, or otherwise modified by", constructor: ParseTextFrame},
"TPA": FrameType{id: "TPA", description: "Part of a set", constructor: ParseTextFrame},
"TPB": FrameType{id: "TPB", description: "Publisher", constructor: ParseTextFrame},
"TRC": FrameType{id: "TRC", description: "ISRC (International Standard Recording Code)", constructor: ParseTextFrame},
"TRD": FrameType{id: "TRD", description: "Recording dates", constructor: ParseTextFrame},
"TRK": FrameType{id: "TRK", description: "Track number/Position in set", constructor: ParseTextFrame},
"TSI": FrameType{id: "TSI", description: "Size", constructor: ParseTextFrame},
"TSS": FrameType{id: "TSS", description: "Software/hardware and settings used for encoding", constructor: ParseTextFrame},
"TT1": FrameType{id: "TT1", description: "Content group description", constructor: ParseTextFrame},
"TT2": FrameType{id: "TT2", description: "Title/Songname/Content description", constructor: ParseTextFrame},
"TT3": FrameType{id: "TT3", description: "Subtitle/Description refinement", constructor: ParseTextFrame},
"TXT": FrameType{id: "TXT", description: "Lyricist/text writer", constructor: ParseTextFrame},
"TXX": FrameType{id: "TXX", description: "User defined text information frame", constructor: ParseDescTextFrame},
"TYE": FrameType{id: "TYE", description: "Year", constructor: ParseTextFrame},
"UFI": FrameType{id: "UFI", description: "Unique file identifier", constructor: ParseDataFrame},
"ULT": FrameType{id: "ULT", description: "Unsychronized lyric/text transcription", constructor: ParseDataFrame},
"WAF": FrameType{id: "WAF", description: "Official audio file webpage", constructor: ParseDataFrame},
"WAR": FrameType{id: "WAR", description: "Official artist/performer webpage", constructor: ParseDataFrame},
"WAS": FrameType{id: "WAS", description: "Official audio source webpage", constructor: ParseDataFrame},
"WCM": FrameType{id: "WCM", description: "Commercial information", constructor: ParseDataFrame},
"WCP": FrameType{id: "WCP", description: "Copyright/Legal information", constructor: ParseDataFrame},
"WPB": FrameType{id: "WPB", description: "Publishers official webpage", constructor: ParseDataFrame},
"WXX": FrameType{id: "WXX", description: "User defined URL link frame", constructor: ParseDataFrame},
}
)
func ParseV22Frame(reader io.Reader) Framer {
data := make([]byte, V22FrameHeaderSize)
if n, err := io.ReadFull(reader, data); n < V22FrameHeaderSize || err != nil {
return nil
}
id := string(data[:3])
t, ok := V22FrameTypeMap[id]
if !ok {
return nil
}
size, err := encodedbytes.NormInt(data[3:6])
if err != nil {
return nil
}
h := FrameHead{
FrameType: t,
size: size,
}
frameData := make([]byte, size)
if n, err := io.ReadFull(reader, frameData); n < int(size) || err != nil {
return nil
}
return t.constructor(h, frameData)
}
func V22Bytes(f Framer) []byte {
headBytes := make([]byte, 0, V22FrameHeaderSize)
headBytes = append(headBytes, f.Id()...)
headBytes = append(headBytes, encodedbytes.NormBytes(uint32(f.Size()))[1:]...)
return append(headBytes, f.Bytes()...)
}

@ -0,0 +1,32 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v2
import (
"bytes"
"testing"
)
func TestV22Frame(t *testing.T) {
textData := []byte{84, 84, 50, 0, 0, 13, 0, 77, 105, 99, 104, 97, 101, 108, 32, 89, 97, 110, 103}
frame := ParseV22Frame(bytes.NewReader(textData))
textFrame, ok := frame.(*TextFrame)
if !ok {
t.Errorf("ParseV23Frame on text data returns wrong type")
}
const text = "Michael Yang"
if ft := textFrame.Text(); ft != text {
t.Errorf("ParseV23Frame incorrect text, expected %s not %s", text, ft)
}
const encoding = "ISO-8859-1"
if e := textFrame.Encoding(); e != encoding {
t.Errorf("ParseV23Frame incorrect encoding, expected %s not %s", encoding, e)
}
if b := V22Bytes(frame); !bytes.Equal(textData, b) {
t.Errorf("V23Bytes produces different byte slice, expected %v not %v", textData, b)
}
}

@ -0,0 +1,164 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v2
import (
"io"
"github.com/akhilrex/podgrab/internal/id3/encodedbytes"
)
var (
// Common frame IDs
V23CommonFrame = map[string]FrameType{
"Title": V23FrameTypeMap["TIT2"],
"Artist": V23FrameTypeMap["TPE1"],
"Album": V23FrameTypeMap["TALB"],
"Year": V23FrameTypeMap["TYER"],
"Genre": V23FrameTypeMap["TCON"],
"Comments": V23FrameTypeMap["COMM"],
"Date": V23FrameTypeMap["TDAT"],
"ReleaseYear": V23FrameTypeMap["TORY"],
}
// V23DeprecatedTypeMap contains deprecated frame IDs from ID3v2.2
V23DeprecatedTypeMap = map[string]string{
"BUF": "RBUF", "COM": "COMM", "CRA": "AENC", "EQU": "EQUA",
"ETC": "ETCO", "GEO": "GEOB", "MCI": "MCDI", "MLL": "MLLT",
"PIC": "APIC", "POP": "POPM", "REV": "RVRB", "RVA": "RVAD",
"SLT": "SYLT", "STC": "SYTC", "TAL": "TALB", "TBP": "TBPM",
"TCM": "TCOM", "TCO": "TCON", "TCR": "TCOP", "TDA": "TDAT",
"TDY": "TDLY", "TEN": "TENC", "TFT": "TFLT", "TIM": "TIME",
"TKE": "TKEY", "TLA": "TLAN", "TLE": "TLEN", "TMT": "TMED",
"TOA": "TOPE", "TOF": "TOFN", "TOL": "TOLY", "TOR": "TORY",
"TOT": "TOAL", "TP1": "TPE1", "TP2": "TPE2", "TP3": "TPE3",
"TP4": "TPE4", "TPA": "TPOS", "TPB": "TPUB", "TRC": "TSRC",
"TRD": "TRDA", "TRK": "TRCK", "TSI": "TSIZ", "TSS": "TSSE",
"TT1": "TIT1", "TT2": "TIT2", "TT3": "TIT3", "TXT": "TEXT",
"TXX": "TXXX", "TYE": "TYER", "UFI": "UFID", "ULT": "USLT",
"WAF": "WOAF", "WAR": "WOAR", "WAS": "WOAS", "WCM": "WCOM",
"WCP": "WCOP", "WPB": "WPB", "WXX": "WXXX",
}
// V23FrameTypeMap specifies the frame IDs and constructors allowed in ID3v2.3
V23FrameTypeMap = map[string]FrameType{
"AENC": FrameType{id: "AENC", description: "Audio encryption", constructor: ParseDataFrame},
"APIC": FrameType{id: "APIC", description: "Attached picture", constructor: ParseImageFrame},
"COMM": FrameType{id: "COMM", description: "Comments", constructor: ParseUnsynchTextFrame},
"COMR": FrameType{id: "COMR", description: "Commercial frame", constructor: ParseDataFrame},
"ENCR": FrameType{id: "ENCR", description: "Encryption method registration", constructor: ParseDataFrame},
"EQUA": FrameType{id: "EQUA", description: "Equalization", constructor: ParseDataFrame},
"ETCO": FrameType{id: "ETCO", description: "Event timing codes", constructor: ParseDataFrame},
"GEOB": FrameType{id: "GEOB", description: "General encapsulated object", constructor: ParseDataFrame},
"GRID": FrameType{id: "GRID", description: "Group identification registration", constructor: ParseDataFrame},
"IPLS": FrameType{id: "IPLS", description: "Involved people list", constructor: ParseDataFrame},
"LINK": FrameType{id: "LINK", description: "Linked information", constructor: ParseDataFrame},
"MCDI": FrameType{id: "MCDI", description: "Music CD identifier", constructor: ParseDataFrame},
"MLLT": FrameType{id: "MLLT", description: "MPEG location lookup table", constructor: ParseDataFrame},
"OWNE": FrameType{id: "OWNE", description: "Ownership frame", constructor: ParseDataFrame},
"PRIV": FrameType{id: "PRIV", description: "Private frame", constructor: ParseDataFrame},
"PCNT": FrameType{id: "PCNT", description: "Play counter", constructor: ParseDataFrame},
"POPM": FrameType{id: "POPM", description: "Popularimeter", constructor: ParseDataFrame},
"POSS": FrameType{id: "POSS", description: "Position synchronisation frame", constructor: ParseDataFrame},
"RBUF": FrameType{id: "RBUF", description: "Recommended buffer size", constructor: ParseDataFrame},
"RVAD": FrameType{id: "RVAD", description: "Relative volume adjustment", constructor: ParseDataFrame},
"RVRB": FrameType{id: "RVRB", description: "Reverb", constructor: ParseDataFrame},
"SYLT": FrameType{id: "SYLT", description: "Synchronized lyric/text", constructor: ParseDataFrame},
"SYTC": FrameType{id: "SYTC", description: "Synchronized tempo codes", constructor: ParseDataFrame},
"TALB": FrameType{id: "TALB", description: "Album/Movie/Show title", constructor: ParseTextFrame},
"TBPM": FrameType{id: "TBPM", description: "BPM (beats per minute)", constructor: ParseTextFrame},
"TCOM": FrameType{id: "TCOM", description: "Composer", constructor: ParseTextFrame},
"TCON": FrameType{id: "TCON", description: "Content type", constructor: ParseTextFrame},
"TCOP": FrameType{id: "TCOP", description: "Copyright message", constructor: ParseTextFrame},
"TDAT": FrameType{id: "TDAT", description: "Date", constructor: ParseTextFrame},
"TDLY": FrameType{id: "TDLY", description: "Playlist delay", constructor: ParseTextFrame},
"TENC": FrameType{id: "TENC", description: "Encoded by", constructor: ParseTextFrame},
"TEXT": FrameType{id: "TEXT", description: "Lyricist/Text writer", constructor: ParseTextFrame},
"TFLT": FrameType{id: "TFLT", description: "File type", constructor: ParseTextFrame},
"TIME": FrameType{id: "TIME", description: "Time", constructor: ParseTextFrame},
"TIT1": FrameType{id: "TIT1", description: "Content group description", constructor: ParseTextFrame},
"TIT2": FrameType{id: "TIT2", description: "Title/songname/content description", constructor: ParseTextFrame},
"TIT3": FrameType{id: "TIT3", description: "Subtitle/Description refinement", constructor: ParseTextFrame},
"TKEY": FrameType{id: "TKEY", description: "Initial key", constructor: ParseTextFrame},
"TLAN": FrameType{id: "TLAN", description: "Language(s)", constructor: ParseTextFrame},
"TLEN": FrameType{id: "TLEN", description: "Length", constructor: ParseTextFrame},
"TMED": FrameType{id: "TMED", description: "Media type", constructor: ParseTextFrame},
"TOAL": FrameType{id: "TOAL", description: "Original album/movie/show title", constructor: ParseTextFrame},
"TOFN": FrameType{id: "TOFN", description: "Original filename", constructor: ParseTextFrame},
"TOLY": FrameType{id: "TOLY", description: "Original lyricist(s)/text writer(s)", constructor: ParseTextFrame},
"TOPE": FrameType{id: "TOPE", description: "Original artist(s)/performer(s)", constructor: ParseTextFrame},
"TORY": FrameType{id: "TORY", description: "Original release year", constructor: ParseTextFrame},
"TOWN": FrameType{id: "TOWN", description: "File owner/licensee", constructor: ParseTextFrame},
"TPE1": FrameType{id: "TPE1", description: "Lead performer(s)/Soloist(s)", constructor: ParseTextFrame},
"TPE2": FrameType{id: "TPE2", description: "Band/orchestra/accompaniment", constructor: ParseTextFrame},
"TPE3": FrameType{id: "TPE3", description: "Conductor/performer refinement", constructor: ParseTextFrame},
"TPE4": FrameType{id: "TPE4", description: "Interpreted, remixed, or otherwise modified by", constructor: ParseTextFrame},
"TPOS": FrameType{id: "TPOS", description: "Part of a set", constructor: ParseTextFrame},
"TPUB": FrameType{id: "TPUB", description: "Publisher", constructor: ParseTextFrame},
"TRCK": FrameType{id: "TRCK", description: "Track number/Position in set", constructor: ParseTextFrame},
"TRDA": FrameType{id: "TRDA", description: "Recording dates", constructor: ParseTextFrame},
"TRSN": FrameType{id: "TRSN", description: "Internet radio station name", constructor: ParseTextFrame},
"TRSO": FrameType{id: "TRSO", description: "Internet radio station owner", constructor: ParseTextFrame},
"TSIZ": FrameType{id: "TSIZ", description: "Size", constructor: ParseTextFrame},
"TSRC": FrameType{id: "TSRC", description: "ISRC (international standard recording code)", constructor: ParseTextFrame},
"TSSE": FrameType{id: "TSSE", description: "Software/Hardware and settings used for encoding", constructor: ParseTextFrame},
"TYER": FrameType{id: "TYER", description: "Year", constructor: ParseTextFrame},
"TXXX": FrameType{id: "TXXX", description: "User defined text information frame", constructor: ParseDescTextFrame},
"UFID": FrameType{id: "UFID", description: "Unique file identifier", constructor: ParseIdFrame},
"USER": FrameType{id: "USER", description: "Terms of use", constructor: ParseDataFrame},
"TCMP": FrameType{id: "TCMP", description: "Part of a compilation (iTunes extension)", constructor: ParseTextFrame},
"USLT": FrameType{id: "USLT", description: "Unsychronized lyric/text transcription", constructor: ParseUnsynchTextFrame},
"WCOM": FrameType{id: "WCOM", description: "Commercial information", constructor: ParseDataFrame},
"WCOP": FrameType{id: "WCOP", description: "Copyright/Legal information", constructor: ParseDataFrame},
"WOAF": FrameType{id: "WOAF", description: "Official audio file webpage", constructor: ParseDataFrame},
"WOAR": FrameType{id: "WOAR", description: "Official artist/performer webpage", constructor: ParseDataFrame},
"WOAS": FrameType{id: "WOAS", description: "Official audio source webpage", constructor: ParseDataFrame},
"WORS": FrameType{id: "WORS", description: "Official internet radio station homepage", constructor: ParseDataFrame},
"WPAY": FrameType{id: "WPAY", description: "Payment", constructor: ParseDataFrame},
"WPUB": FrameType{id: "WPUB", description: "Publishers official webpage", constructor: ParseDataFrame},
"WXXX": FrameType{id: "WXXX", description: "User defined URL link frame", constructor: ParseDataFrame},
}
)
func ParseV23Frame(reader io.Reader) Framer {
data := make([]byte, FrameHeaderSize)
if n, err := io.ReadFull(reader, data); n < FrameHeaderSize || err != nil {
return nil
}
id := string(data[:4])
t, ok := V23FrameTypeMap[id]
if !ok {
return nil
}
size, err := encodedbytes.NormInt(data[4:8])
if err != nil {
return nil
}
h := FrameHead{
FrameType: t,
statusFlags: data[8],
formatFlags: data[9],
size: size,
}
frameData := make([]byte, size)
if n, err := io.ReadFull(reader, frameData); n < int(size) || err != nil {
return nil
}
return t.constructor(h, frameData)
}
func V23Bytes(f Framer) []byte {
headBytes := make([]byte, 0, FrameHeaderSize)
headBytes = append(headBytes, f.Id()...)
headBytes = append(headBytes, encodedbytes.NormBytes(uint32(f.Size()))...)
headBytes = append(headBytes, f.StatusFlags(), f.FormatFlags())
return append(headBytes, f.Bytes()...)
}

@ -0,0 +1,32 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v2
import (
"bytes"
"testing"
)
func TestV23Frame(t *testing.T) {
textData := []byte{84, 80, 69, 49, 0, 0, 0, 13, 0, 0, 0, 77, 105, 99, 104, 97, 101, 108, 32, 89, 97, 110, 103}
frame := ParseV23Frame(bytes.NewReader(textData))
textFrame, ok := frame.(*TextFrame)
if !ok {
t.Errorf("ParseV23Frame on text data returns wrong type")
}
const text = "Michael Yang"
if ft := textFrame.Text(); ft != text {
t.Errorf("ParseV23Frame incorrect text, expected %s not %s", text, ft)
}
const encoding = "ISO-8859-1"
if e := textFrame.Encoding(); e != encoding {
t.Errorf("ParseV23Frame incorrect encoding, expected %s not %s", encoding, e)
}
if b := V23Bytes(frame); !bytes.Equal(textData, b) {
t.Errorf("V23Bytes produces different byte slice, expected %v not %v", textData, b)
}
}

@ -0,0 +1,8 @@
// Copyright 2013 Michael Yang. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package v2
func isBitSet(flag, index byte) bool {
return flag&(1<<index) != 0
}

@ -197,11 +197,11 @@ func createFolder(folder string, parent string) string {
func createDataFolderIfNotExists(folder string) string {
dataPath := os.Getenv("DATA")
return createFolder(dataPath, folder)
return createFolder(folder, dataPath)
}
func createConfigFolderIfNotExists(folder string) string {
dataPath := os.Getenv("CONFIG")
return createFolder(dataPath, folder)
configPath := os.Getenv("CONFIG")
return createFolder(folder, configPath)
}
func getFileName(link string, title string, defaultExtension string) string {

@ -12,6 +12,7 @@ import (
"time"
"github.com/akhilrex/podgrab/db"
id3 "github.com/akhilrex/podgrab/internal/id3"
"github.com/akhilrex/podgrab/model"
strip "github.com/grokify/html-strip-tags-go"
"go.uber.org/zap"
@ -318,6 +319,7 @@ func DownloadMissingEpisodes() error {
defer wg.Done()
url, _ := Download(item.FileURL, item.Title, item.Podcast.Title, GetPodcastPrefix(&item, &setting))
SetPodcastItemAsDownloaded(item.ID, url)
go SetId3Tags(url, &item)
}(item, *setting)
if index%5 == 0 {
@ -377,6 +379,7 @@ func DownloadSingleEpisode(podcastItemId string) error {
if err != nil {
return err
}
go SetId3Tags(url, &podcastItem)
return SetPodcastItemAsDownloaded(podcastItem.ID, url)
}
@ -501,3 +504,26 @@ func UpdateSettings(downloadOnAdd bool, initialDownloadCount int, autoDownload b
func UnlockMissedJobs() {
db.UnlockMissedJobs()
}
func SetId3Tags(path string, item *db.PodcastItem) {
file, err := id3.Open(path, false)
if err != nil {
fmt.Println(err.Error())
return
}
if file.Title() == "" {
file.SetTitle(item.Title)
}
if file.Artist() == "" {
file.SetArtist(item.Podcast.Title)
}
if file.Album() == "" {
file.SetAlbum(item.Podcast.Title)
}
if len(file.Comments()) == 0 {
file.SetComment(item.Summary)
}
file.SetGenre("Podcast")
file.SetYear(strconv.Itoa(item.PubDate.Year()))
defer file.Close()
}

Loading…
Cancel
Save