-
Notifications
You must be signed in to change notification settings - Fork 51
/
tag.go
447 lines (381 loc) · 11.8 KB
/
tag.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
// Copyright 2016 Albert Nigmatzianov. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package id3v2
import (
"errors"
"io"
"os"
)
var ErrNoFile = errors.New("tag was not initialized with file")
// Tag stores all information about opened tag.
type Tag struct {
frames map[string]Framer
sequences map[string]*sequence
defaultEncoding Encoding
reader io.Reader
originalSize int64
version byte
}
// AddFrame adds f to tag with appropriate id. If id is "" or f is nil,
// AddFrame will not add it to tag.
//
// If you want to add attached picture, comment or unsynchronised lyrics/text
// transcription frames, better use AddAttachedPicture, AddCommentFrame
// or AddUnsynchronisedLyricsFrame methods respectively.
func (tag *Tag) AddFrame(id string, f Framer) {
if id == "" || f == nil {
return
}
if mustFrameBeInSequence(id) {
sequence := tag.sequences[id]
if sequence == nil {
sequence = getSequence()
}
sequence.AddFrame(f)
tag.sequences[id] = sequence
} else {
tag.frames[id] = f
}
}
// AddAttachedPicture adds the picture frame to tag.
func (tag *Tag) AddAttachedPicture(pf PictureFrame) {
tag.AddFrame(tag.CommonID("Attached picture"), pf)
}
// AddCommentFrame adds the comment frame to tag.
func (tag *Tag) AddCommentFrame(cf CommentFrame) {
tag.AddFrame(tag.CommonID("Comments"), cf)
}
// AddTextFrame creates the text frame with provided encoding and text
// and adds to tag.
func (tag *Tag) AddTextFrame(id string, encoding Encoding, text string) {
tag.AddFrame(id, TextFrame{Encoding: encoding, Text: text})
}
// AddUnsynchronisedLyricsFrame adds the unsynchronised lyrics/text frame
// to tag.
func (tag *Tag) AddUnsynchronisedLyricsFrame(uslf UnsynchronisedLyricsFrame) {
tag.AddFrame(tag.CommonID("Unsynchronised lyrics/text transcription"), uslf)
}
// AddUserDefinedTextFrame adds the custom frame (TXXX) to tag.
func (tag *Tag) AddUserDefinedTextFrame(udtf UserDefinedTextFrame) {
tag.AddFrame(tag.CommonID("User defined text information frame"), udtf)
}
// AddUFIDFrame adds the unique file identifier frame (UFID) to tag.
func (tag *Tag) AddUFIDFrame(ufid UFIDFrame) {
tag.AddFrame(tag.CommonID("Unique file identifier"), ufid)
}
// CommonID returns frame ID from given description.
// For example, CommonID("Language") will return "TLAN".
// If it can't find the ID with given description, it returns the description.
//
// All descriptions you can find in file common_ids.go
// or in id3 documentation.
// v2.3: http://id3.org/id3v2.3.0#Declared_ID3v2_frames
// v2.4: http://id3.org/id3v2.4.0-frames
func (tag *Tag) CommonID(description string) string {
var ids map[string]string
if tag.version == 3 {
ids = V23CommonIDs
} else {
ids = V24CommonIDs
}
if id, ok := ids[description]; ok {
return id
}
return description
}
// AllFrames returns map, that contains all frames in tag, that could be parsed.
// The key of this map is an ID of frame and value is an array of frames.
func (tag *Tag) AllFrames() map[string][]Framer {
frames := make(map[string][]Framer)
for id, f := range tag.frames {
frames[id] = []Framer{f}
}
for id, sequence := range tag.sequences {
frames[id] = sequence.Frames()
}
return frames
}
// DeleteAllFrames deletes all frames in tag.
func (tag *Tag) DeleteAllFrames() {
if tag.frames == nil || len(tag.frames) > 0 {
tag.frames = make(map[string]Framer)
}
if tag.sequences == nil || len(tag.sequences) > 0 {
for _, s := range tag.sequences {
putSequence(s)
}
tag.sequences = make(map[string]*sequence)
}
}
// DeleteFrames deletes frames in tag with given id.
func (tag *Tag) DeleteFrames(id string) {
delete(tag.frames, id)
if s, ok := tag.sequences[id]; ok {
putSequence(s)
delete(tag.sequences, id)
}
}
// Reset deletes all frames in tag and parses rd considering opts.
func (tag *Tag) Reset(rd io.Reader, opts Options) error {
tag.DeleteAllFrames()
return tag.parse(rd, opts)
}
// GetFrames returns frames with corresponding id.
// It returns nil if there is no frames with given id.
func (tag *Tag) GetFrames(id string) []Framer {
if f, exists := tag.frames[id]; exists {
return []Framer{f}
} else if s, exists := tag.sequences[id]; exists {
return s.Frames()
}
return nil
}
// GetLastFrame returns last frame from slice, that is returned from GetFrames function.
// GetLastFrame is suitable for frames, that can be only one in whole tag.
// For example, for text frames.
func (tag *Tag) GetLastFrame(id string) Framer {
// Avoid an allocation of slice in GetFrames,
// if there is anyway one frame.
if f, exists := tag.frames[id]; exists {
return f
}
fs := tag.GetFrames(id)
if len(fs) == 0 {
return nil
}
return fs[len(fs)-1]
}
// GetTextFrame returns text frame with corresponding id.
func (tag *Tag) GetTextFrame(id string) TextFrame {
f := tag.GetLastFrame(id)
if f == nil {
return TextFrame{}
}
tf := f.(TextFrame)
return tf
}
// DefaultEncoding returns default encoding of tag.
// Default encoding is used in methods (e.g. SetArtist, SetAlbum ...) for
// setting text frames without the explicit providing of encoding.
func (tag *Tag) DefaultEncoding() Encoding {
return tag.defaultEncoding
}
// SetDefaultEncoding sets default encoding for tag.
// Default encoding is used in methods (e.g. SetArtist, SetAlbum ...) for
// setting text frames without explicit providing encoding.
func (tag *Tag) SetDefaultEncoding(encoding Encoding) {
tag.defaultEncoding = encoding
}
func (tag *Tag) setDefaultEncodingBasedOnVersion(version byte) {
if version == 4 {
tag.SetDefaultEncoding(EncodingUTF8)
} else {
tag.SetDefaultEncoding(EncodingISO)
}
}
// Count returns the number of frames in tag.
func (tag *Tag) Count() int {
n := len(tag.frames)
for _, s := range tag.sequences {
n += s.Count()
}
return n
}
// HasFrames checks if there is at least one frame in tag.
// It's much faster than tag.Count() > 0.
func (tag *Tag) HasFrames() bool {
return len(tag.frames) > 0 || len(tag.sequences) > 0
}
func (tag *Tag) Title() string {
return tag.GetTextFrame(tag.CommonID("Title")).Text
}
func (tag *Tag) SetTitle(title string) {
tag.AddTextFrame(tag.CommonID("Title"), tag.DefaultEncoding(), title)
}
func (tag *Tag) Artist() string {
return tag.GetTextFrame(tag.CommonID("Artist")).Text
}
func (tag *Tag) SetArtist(artist string) {
tag.AddTextFrame(tag.CommonID("Artist"), tag.DefaultEncoding(), artist)
}
func (tag *Tag) Album() string {
return tag.GetTextFrame(tag.CommonID("Album/Movie/Show title")).Text
}
func (tag *Tag) SetAlbum(album string) {
tag.AddTextFrame(tag.CommonID("Album/Movie/Show title"), tag.DefaultEncoding(), album)
}
func (tag *Tag) Year() string {
return tag.GetTextFrame(tag.CommonID("Year")).Text
}
func (tag *Tag) SetYear(year string) {
tag.AddTextFrame(tag.CommonID("Year"), tag.DefaultEncoding(), year)
}
func (tag *Tag) Genre() string {
return tag.GetTextFrame(tag.CommonID("Content type")).Text
}
func (tag *Tag) SetGenre(genre string) {
tag.AddTextFrame(tag.CommonID("Content type"), tag.DefaultEncoding(), genre)
}
// iterateOverAllFrames iterates over every single frame in tag and calls
// f for them. It consumps no memory at all, unlike the tag.AllFrames().
// It returns error only if f returns error.
func (tag *Tag) iterateOverAllFrames(f func(id string, frame Framer) error) error {
for id, frame := range tag.frames {
if err := f(id, frame); err != nil {
return err
}
}
for id, sequence := range tag.sequences {
for _, frame := range sequence.Frames() {
if err := f(id, frame); err != nil {
return err
}
}
}
return nil
}
// Size returns the size of tag (tag header + size of all frames) in bytes.
func (tag *Tag) Size() int {
if !tag.HasFrames() {
return 0
}
var n int
n += tagHeaderSize // Add the size of tag header
tag.iterateOverAllFrames(func(id string, f Framer) error {
n += frameHeaderSize + f.Size() // Add the whole frame size
return nil
})
return n
}
// Version returns current ID3v2 version of tag.
func (tag *Tag) Version() byte {
return tag.version
}
// SetVersion sets given ID3v2 version to tag.
// If version is less than 3 or greater than 4, then this method will do nothing.
// If tag has some frames, which are deprecated or changed in given version,
// then to your notice you can delete, change or just stay them.
func (tag *Tag) SetVersion(version byte) {
if version < 3 || version > 4 {
return
}
tag.version = version
tag.setDefaultEncodingBasedOnVersion(version)
}
// Save writes tag to the file, if tag was opened with a file.
// If there are no frames in tag, Save will write
// only music part without any ID3v2 information.
// If tag was initiliazed not with file, it returns ErrNoFile.
func (tag *Tag) Save() error {
file, ok := tag.reader.(*os.File)
if !ok {
return ErrNoFile
}
// Get original file mode.
originalFile := file
originalStat, err := originalFile.Stat()
if err != nil {
return err
}
// Create a temp file for mp3 file, which will contain new tag.
name := file.Name() + "-id3v2"
newFile, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE, originalStat.Mode())
if err != nil {
return err
}
// Make sure we clean up the temp file if it's still around.
// tempfileShouldBeRemoved created only for performance
// improvement to prevent calling redundant Remove syscalls if file is moved
// and is not need to be removed.
tempfileShouldBeRemoved := true
defer func() {
if tempfileShouldBeRemoved {
os.Remove(newFile.Name())
}
}()
// Write tag in new file.
tagSize, err := tag.WriteTo(newFile)
if err != nil {
return err
}
// Seek to a music part of original file.
if _, err = originalFile.Seek(tag.originalSize, os.SEEK_SET); err != nil {
return err
}
// Write to new file the music part.
buf := getByteSlice(128 * 1024)
defer putByteSlice(buf)
if _, err = io.CopyBuffer(newFile, originalFile, buf); err != nil {
return err
}
// Close files to allow replacing.
newFile.Close()
originalFile.Close()
// Replace original file with new file.
if err = os.Rename(newFile.Name(), originalFile.Name()); err != nil {
return err
}
tempfileShouldBeRemoved = false
// Set tag.reader to new file with original name.
tag.reader, err = os.Open(originalFile.Name())
if err != nil {
return err
}
// Set tag.originalSize to new frames size.
tag.originalSize = tagSize
return nil
}
// WriteTo writes whole tag in w if there is at least one frame.
// It returns the number of bytes written and error during the write.
// It returns nil as error if the write was successful.
func (tag *Tag) WriteTo(w io.Writer) (n int64, err error) {
if w == nil {
return 0, errors.New("w is nil")
}
// Count size of frames.
framesSize := tag.Size() - tagHeaderSize
if framesSize <= 0 {
return 0, nil
}
// Write tag header.
bw := getBufWriter(w)
defer putBufWriter(bw)
writeTagHeader(bw, uint(framesSize), tag.version)
// Write frames.
synchSafe := tag.Version() == 4
err = tag.iterateOverAllFrames(func(id string, f Framer) error {
return writeFrame(bw, id, f, synchSafe)
})
if err != nil {
bw.Flush()
return int64(bw.Written()), err
}
return int64(bw.Written()), bw.Flush()
}
func writeTagHeader(bw *bufWriter, framesSize uint, version byte) {
bw.Write(id3Identifier)
bw.WriteByte(version)
bw.WriteByte(0) // Revision
bw.WriteByte(0) // Flags
bw.WriteBytesSize(framesSize, true)
}
func writeFrame(bw *bufWriter, id string, frame Framer, synchSafe bool) error {
writeFrameHeader(bw, id, uint(frame.Size()), synchSafe)
_, err := frame.WriteTo(bw)
return err
}
func writeFrameHeader(bw *bufWriter, id string, frameSize uint, synchSafe bool) {
bw.WriteString(id)
bw.WriteBytesSize(frameSize, synchSafe)
bw.Write([]byte{0, 0}) // Flags
}
// Close closes tag's file, if tag was opened with a file.
// If tag was initiliazed not with file, it returns ErrNoFile.
func (tag *Tag) Close() error {
file, ok := tag.reader.(*os.File)
if !ok {
return ErrNoFile
}
return file.Close()
}