• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

benrr101 / node-taglib-sharp / 46462135

pending completion
46462135

push

appveyor

Benjamin Russell
Merge branch 'release/v5.1.0'

3096 of 3788 branches covered (81.73%)

Branch coverage included in aggregate %.

2171 of 2171 new or added lines in 47 files covered. (100.0%)

25320 of 26463 relevant lines covered (95.68%)

437.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.16
/src/id3v2/frames/textInformationFrame.ts
1
import Genres from "../../genres";
1✔
2
import Id3v2Settings from "../id3v2Settings";
1✔
3
import {ByteVector, StringType} from "../../byteVector";
1✔
4
import {Frame, FrameClassType} from "./frame";
1✔
5
import {Id3v2FrameHeader} from "./frameHeader";
1✔
6
import {FrameIdentifier, FrameIdentifiers} from "../frameIdentifiers";
1✔
7
import {Guards, StringComparison, StringUtils} from "../../utils";
1✔
8

9
/**
10
 * This class provides support for ID3v2 text information frames (section 4.2) covering `T000` to
11
 * `TZZZ`, excluding `TXXX`.
12
 * Text information frames contain the most commonly used values in tagging, including the artist,
13
 * track name, and just about any value that can be expressed as text. The following table contains
14
 * types and descriptions as found in the ID3 2.4.0 native frames specification (Copyright Martin
15
 * Nilsson 2000).
16
 * * TIT1 - The "Content Group Description" frame is used if the sound belongs to a larger category
17
 *   of sounds/music. For example, classical music is often sorted in different musical sections
18
 *   (eg. "Piano Concerto", "Weather - Hurricane").
19
 * * TIT2 - The "Title/Song name/Content description" frame is the actual name of the piece (eg.
20
 *   "Adagio", "Hurricane Donna").
21
 * * TIT3 - The "Subtitle/Description refinement" frame is used for information directly related to
22
 *   the contents title (eg. "Op. 16" or "Performed Live at Wembley").
23
 * * TALB - The "Album/Movie/Show title" frame is intended for the title of the recording (or
24
 *   source of sound) from which the audio in the file is taken.
25
 * * TOAL - The "Original album/movie/show title" frame is intended for the title of the original
26
 *   recording (or source of sound), if for example the music in the file should be a cover of a
27
 *   previously released song.
28
 * * TRCK - The "Track number/Position in set" frame is a numeric string containing the order
29
 *   number of the audio-file on its original recording. This MAY be extended with a "/" character
30
 *   and a numeric string containing the total number of tracks/elements on the original recording
31
 *   (eg "4/9").
32
 * * TPOS - The "Part of a set" frame is a numeric string that describes which part of a set the
33
 *   audio came from. This frame is used if the source described in the "TLAB" frame is divided
34
 *   into several mediums, eg. a double CD. The value MAY be extended with a "/" character and a
35
 *   numeric string containing the total number of parts in the set (eg. "1/2").
36
 * * TSST - The "Set Subtitle" frame is intended for the subtitle of the part of a set this track
37
 *   belongs to.
38
 * * TSRC - The "ISRC" frame should contain the International Standard Recording Code (12 chars).
39
 * * TPE1 - The "Lead artist/Lead performer/Soloist/Performing Group" frame is used for the main
40
 *   artist.
41
 * * TPE2 - The "Band/Orchestra/Accompaniment" frame is used for additional information about the
42
 *   performers in the recording.
43
 * * TPE3 - The "Conductor" frame is used for the name of the conductor.
44
 * * TPE4 - The "Interpreted, remixed, or otherwise modified by" frame contains more information
45
 *   about the people behind a remix and similar interpretations of another existing piece.
46
 * * TOPE - The "Original artist/Performer" frame is intended for the performer of the original
47
 *   recording, if for example the music in the file should be a cover of a previously released
48
 *   song.
49
 * * TEXT - The "Lyricist/Text writer" frame is intended for the writer of the text or lyrics in
50
 *   the recording.
51
 * * TOLY - The "Original lyricist/Text writer" frame is intended for the text writer of the
52
 *   original recording, if for example the music in the file should be a cover of a previously
53
 *   released song.
54
 * * TCOM - The "composer" frame is intended for the name of the composer.
55
 * * TMCL - The "musician credits list" frame is intended as a mapping betweenInclusive instruments and the
56
 *   musician who played it. Every odd field is an instrument and every even is an artist of a comma
57
 *   delimited list of artists.
58
 * * TIPL - The "Involved people list" frame is very similar to the musician credits list, but maps
59
 *   betweenInclusive functions, like producer, and names.
60
 * * TENC - The "Encoded by" frame contains the name of the person or organization that encoded the
61
 *   audio file. This field may contain a copyright message, if the audio file is also copyrighted
62
 *   by the encoder.
63
 * * TBPM - The "BPM" frame contains the number of beats per minute in the main part of the audio.
64
 *   The BPM is an integer and represented as a numeric string.
65
 * * TLEN - The "Length" frame contains the length of the audio file in milliseconds, represented
66
 *   as a numeric string.
67
 * * TKEY - The "Initial key" frame contains the musical key in which the sound starts. It is
68
 *   represented as a string with a maximum length of 3 characters. The ground keys are represented
69
 *   with "A" - "G" and half keys are represented with "b" or "#". Minor is represented as "m", eg.
70
 *   "Dbm". Off key is represented with an "o" only.
71
 * * TLAN - The "language" frame should contain the languages of the text or lyrics spoken or sung
72
 *   in the audio. The language is represented with three characters according to ISO-639-2. If
73
 *   more than one language is used in the text, the language codes should follow according to the
74
 *   amount of usage.
75
 * * TCON - The "Content type" frame, which in ID3v1 was stored as one byte numeric value only, is
76
 *   now a string. You may use one or several of the ID3v1 types as numeric strings, or, since the
77
 *   category list would be impossible to maintain with accurate and up to date categories, define
78
 *   your own.
79
 * * TFLT - The "File type" frame indicates which type of audio this tag defines. (see the
80
 *   specification for more details)
81
 * * TMED - The "Media type" frame describes from which media the sound originated. (see the
82
 *   specification for more details)
83
 * * TMOO - The "mood" frame is intended to reflect the mood of the audio with a few keywords (eg.
84
 *   "Romantic" or "Sad").
85
 * * TCOP - The "Copyright message" frame, in which the string must begin with a year and a space
86
 *   character (making 5 characters), is intended for the copyright holder of the original sound,
87
 *   not the audio file itself. The absence of this frame means only that the copyright information
88
 *   is unavailable or has been removed, and must not be interpreted to mean that the audio is
89
 *   public domain. Every time this field is displayed, the field must be preceded with
90
 *   "Copyright " (C) " ", where (C) is one character showing the copyright mark.
91
 * * TPRO - The "Produced notice" frame, in which the string must begin with a year and a space
92
 *   character (making 5 characters), is intended for the production copyright holder of the
93
 *   production copyright holder of the original sound, not the audio file itself. Every time this
94
 *   field is displayed, the field must be preceded with "Produced " (P) " ", where (P) is one
95
 *   character showing the sound recording copyright symbol.
96
 * * TPUB - The "Publisher" frame contains the name of the label or publisher.
97
 * * TOWN - The "file owner/licensee" frame containing the name of the owner or licensee of the
98
 *   file and its contents.
99
 * * TRSN - The "Internet radio station name" frame contains the name of the internet radio
100
 *   station from which the audio is streamed.
101
 * * TRSO - The "Internet radio station owner" frame contains the name of the owner of the internet
102
 *   radio station from which the audio is streamed.
103
 * * TOFN - The "Original filename" frame contains the preferred filename for the file, since some
104
 *   media doesn't allow the desired length of the filename. The filename is case sensitive and
105
 *   includes its extension.
106
 * * TDLY - The "Playlist delay" frame defines the numbers of milliseconds of silence that should
107
 *   be inserted before this audio. The value zero indicates that this is a part of a multi-file
108
 *   audio track that should be played continuously.
109
 * * TDEN - The "Encoding time" frame contains a timestamp describing when the audio was encoded.
110
 *   Timestamp format is described in the ID3v2 structure document.
111
 * * TDOR - The "Original release time" frame contains a timestamp describing when the original
112
 *   recording was released. Timestamp format is described in the ID3v2 structure document.
113
 * * TDRC - The "Recording time" frame contains a timestamp describing when the audio was recorded.
114
 *   Timestamp format is described in the ID3v2 structure document.
115
 * * TDRL - The "Release time" frame contains a timestamp describing when the audio was first
116
 *   released. Timestamp format is described in the ID3v2 structure document.
117
 * * TDTG - The "Tagging time" frame contains a timestamp describing when the audio was tagged.
118
 *   Timestamp format is described in the ID3v2 structure document.
119
 * * TSSE - The "Software/Hardware and settings used for encoding" frame includes the used audio
120
 *   encoder and its settings when the file was encoded. Hardware refers to hardware encoders, not
121
 *   the computer on which a program was ran.
122
 * * TSOA - The "Album sort order" frame defines a string which should be used instead of the album
123
 *   name (TALB) for sorting purposes. For example, an album named "A Soundtrack" might be
124
 *   preferably sorted as "Soundtrack".
125
 * * TSOP - The "Performer sort order" frame defines a string which should be used instead of the
126
 *   performer (TPE2) for sorting purposes.
127
 * * TSOT - The "Title sort order" frame defines a string which should be used instead of the title
128
 *   (TIT2) for sorting purposes.
129
 */
130
export class TextInformationFrame extends Frame {
1✔
131
    private static readonly COVER_ABBREV = "CR";
1✔
132
    private static readonly COVER_STRING = "Cover";
1✔
133
    private static readonly REMIX_ABBREV = "RX";
1✔
134
    private static readonly REMIX_STRING = "Remix";
1✔
135
    private static readonly SPLIT_FRAME_TYPES = [
1✔
136
        FrameIdentifiers.TCOM,
137
        FrameIdentifiers.TEXT,
138
        FrameIdentifiers.TMCL,
139
        FrameIdentifiers.TOLY,
140
        FrameIdentifiers.TOPE,
141
        FrameIdentifiers.TSOC,
142
        FrameIdentifiers.TSOP,
143
        FrameIdentifiers.TSO2,
144
        FrameIdentifiers.TPE1,
145
        FrameIdentifiers.TPE2,
146
        FrameIdentifiers.TPE3,
147
        FrameIdentifiers.TPE4
148
    ];
149

150
    // @TODO: no protected access to members
151
    protected _encoding: StringType = Id3v2Settings.defaultEncoding;
366✔
152
    protected _rawData: ByteVector;
153
    protected _rawVersion: number;
154
    protected _textFields: string[] = [];
366✔
155

156
    // #region Constructors
157

158
    protected constructor(header: Id3v2FrameHeader) {
159
        super(header);
366✔
160
    }
161

162
    /**
163
     * Constructs and initializes a new instance with a specified identifier
164
     * @param identifier Byte vector containing the identifier for the frame
165
     * @param encoding Optionally, the encoding to use for the new instance. If omitted, defaults
166
     *     to {@link Id3v2Settings.defaultEncoding}
167
     */
168
    public static fromIdentifier(
169
        identifier: FrameIdentifier,
170
        encoding: StringType = Id3v2Settings.defaultEncoding
201✔
171
    ): TextInformationFrame {
172
        const frame = new TextInformationFrame(new Id3v2FrameHeader(identifier));
204✔
173
        frame._encoding = encoding;
204✔
174
        return frame;
204✔
175
    }
176

177
    /**
178
     * Constructs and initializes a new instance by reading its raw data in a specified ID3v2
179
     * version. This method allows for offset reading from the data byte vector.
180
     * @param data Raw representation of the new frame
181
     * @param offset What offset in `data` the frame actually begins. Must be positive,
182
     *     safe integer
183
     * @param header Header of the frame found at `data` in the data
184
     * @param version ID3v2 version the frame was originally encoded with
185
     */
186
    public static fromOffsetRawData(
187
        data: ByteVector,
188
        offset: number,
189
        header: Id3v2FrameHeader,
190
        version: number
191
    ): TextInformationFrame {
192
        Guards.truthy(data, "data");
43✔
193
        Guards.uint(offset, "offset");
41✔
194
        Guards.truthy(header, "header");
36✔
195

196
        const frame = new TextInformationFrame(header);
34✔
197
        frame.setData(data, offset, false, version);
34✔
198
        return frame;
34✔
199
    }
200

201
    /**
202
     * Constructs and initializes a new instance by reading its raw data in a specified
203
     * ID3v2 version.
204
     * @param data Raw representation of the new frame
205
     * @param version ID3v2 version the raw frame is encoded with, must be a positive 8-bit integer
206
     */
207
    public static fromRawData(data: ByteVector, version: number): TextInformationFrame {
208
        Guards.truthy(data, "data");
31✔
209
        Guards.byte(version, "version");
29✔
210

211
        const frame = new TextInformationFrame(Id3v2FrameHeader.fromData(data, version));
26✔
212
        frame.setData(data, 0, true, version);
26✔
213
        return frame;
26✔
214
    }
215

216
    // #endregion
217

218
    // #region Properties
219

220
    /** @inheritDoc */
221
    public get frameClassType(): FrameClassType { return FrameClassType.TextInformationFrame; }
1,038✔
222

223
    /**
224
     * Gets the text contained in the current instance.
225
     * Note: Modifying the contents of the returned value will not modify the contents of the
226
     * current instance. The value must be reassigned for the value to change.
227
     */
228
    public get text(): string[] {
229
        this.parseRawData();
517✔
230
        return this._textFields.slice();
517✔
231
    }
232
    /**
233
     * Sets the text contained in the current instance.
234
     */
235
    public set text(value: string[]) {
236
        this.parseRawData();
339✔
237
        this._textFields = value ? value.slice() : [];
339!
238
    }
239

240
    /**
241
     * Gets the text encoding to use when rendering the current instance.
242
     */
243
    public get textEncoding(): StringType {
244
        this.parseRawData();
220✔
245
        return this._encoding;
220✔
246
    }
247
    /**
248
     * Sets the text encoding to use when rendering the current instance.
249
     * This value will be overridden if {@link Id3v2Settings.forceDefaultEncoding} is `true`.
250
     */
251
    public set textEncoding(value: StringType) {
252
        this.parseRawData();
185✔
253
        this._encoding = value;
185✔
254
    }
255

256
    // #endregion
257

258
    // #region Public Methods
259

260
    /**
261
     * Gets a {@link TextInformationFrame} object of a specified type from a specified type from a
262
     * list of text information frames.
263
     * @param frames List of frames to search
264
     * @param ident Frame identifier to search for
265
     * @returns Matching frame if it exists in `tag`, `undefined` if a matching frame was not found
266
     */
267
    public static findTextInformationFrame(
268
        frames: TextInformationFrame[],
269
        ident: FrameIdentifier
270
    ): TextInformationFrame {
271
        Guards.truthy(frames, "frames");
1,938✔
272
        Guards.truthy(ident, "ident");
1,936✔
273

274
        return frames.find((f) => f.frameId === ident);
1,934✔
275
    }
276

277
    /** @inheritDoc */
278
    public clone(): Frame {
279
        const frame = TextInformationFrame.fromIdentifier(this.frameId, this._encoding);
2✔
280
        frame._textFields = this._textFields.slice();
2✔
281
        if (this._rawData) {
2✔
282
            frame._rawData = this._rawData.toByteVector();
1✔
283
        }
284
        frame._rawVersion = this._rawVersion;
2✔
285
        return frame;
2✔
286
    }
287

288
    /**
289
     * Renders the current instance, encoded in a specified ID3v2 version.
290
     * @param version ID3v2 version to use when encoding the current instance. Must be a positive
291
     *     8-bit integer.
292
     * @returns Rendered version of the current instance.
293
     */
294
    public render(version: number): ByteVector {
295
        Guards.byte(version, "version");
213✔
296

297
        if (version !== 3 || this.frameId !== FrameIdentifiers.TDRC) {
210!
298
            return super.render(version);
210✔
299
        }
300

301
        const text = this.toString();
×
302
        if (text.length < 10 || text[4] !== "-" || text[7] !== "-") {
×
303
            return super.render(version);
×
304
        }
305

306
        const output = ByteVector.empty();
×
307
        let frame = new TextInformationFrame(new Id3v2FrameHeader(FrameIdentifiers.TYER));
×
308
        frame.text = [text.substring(0, 4)];
×
309
        output.addByteVector(frame.render(version));
×
310

311
        frame = new TextInformationFrame(new Id3v2FrameHeader(FrameIdentifiers.TDAT));
×
312
        frame.text = [text.substring(5, 7) + text.substring(8, 10)];
×
313
        output.addByteVector(frame.render(version));
×
314

315
        if (text.length < 16 || text[10] !== "T" || text[13] !== ":") {
×
316
            return output;
×
317
        }
318

319
        frame = new TextInformationFrame(new Id3v2FrameHeader(FrameIdentifiers.TIME));
×
320
        frame.text = [text.substring(11, 13) + text.substring(14, 16)];
×
321
        output.addByteVector(frame.render(version));
×
322

323
        return output;
×
324
    }
325

326
    /**
327
     * Returns a text representation of the current instance by combining the text with semicolons.
328
     */
329
    public toString(): string {
330
        this.parseRawData();
117✔
331
        return this.text.join("; ");
117✔
332
    }
333

334
    // #endregion
335

336
    // #region Protected Methods
337

338
    /** @inheritDoc */
339
    protected parseFields(data: ByteVector, version: number): void {
340
        this._rawData = data;
89✔
341
        this._rawVersion = version;
89✔
342
    }
343

344
    /**
345
     * Performs the actual parsing of the raw data.
346
     * Because of the high parsing cost and relatively low usage of the class {@link parseFields}
347
     * only stores the field data so it can be parsed on demand. Whenever a property or method is
348
     * called which requires the data, this method is called, and only on the first call does it
349
     * actually parse the data.
350
     */
351
    protected parseRawData(): void {
352
        if (!this._rawData) {
1,378✔
353
            return;
1,324✔
354
        }
355

356
        const data = this._rawData;
54✔
357
        this._rawData = undefined;
54✔
358

359
        // Read the string data type (first byte of the field data)
360
        this._encoding = data.get(0);
54✔
361

362
        const fieldList = [];
54✔
363
        const delim = ByteVector.getTextDelimiter(this._encoding);
54✔
364

365
        if (this._rawVersion > 3 && this.frameId === FrameIdentifiers.TCON) {
54✔
366
            // TCON on ID3v2.4 is encoded as a separate field for each genre. Fields can either be
367
            // the old numeric ID3v1 genres (no parenthesis) or free text. RX/CR can also be used.
368
            // This way is **much** better...
369
            const genres = data.subarray(1).toStrings(this._encoding);
1✔
370
            const textGenres = genres.map((g) => {
1✔
371
                switch (g) {
3✔
372
                    case TextInformationFrame.COVER_ABBREV:
3!
373
                        return TextInformationFrame.COVER_STRING;
×
374
                    case TextInformationFrame.REMIX_ABBREV:
375
                        return TextInformationFrame.REMIX_STRING;
×
376
                    default:
377
                        const textGenre = Genres.indexToAudio(g, false);
3✔
378
                        return textGenre || g;
3✔
379
                }
380
            });
381
            fieldList.push(...textGenres);
1✔
382
        } else if (this._rawVersion > 3 || this.frameId === FrameIdentifiers.TXXX) {
53✔
383
            fieldList.push(...data.subarray(1).toStrings(this._encoding));
33✔
384
        } else if (data.length > 1 && !data.containsAt(delim, 1)) {
20✔
385
            let value = data.subarray(1).toString(this._encoding);
18✔
386

387
            // Truncate values containing NULL bytes
388
            const nullIndex = value.indexOf("\x00");
18✔
389
            if (nullIndex >= 0) {
18!
390
                value = value.substring(0, nullIndex);
×
391
            }
392

393
            if (TextInformationFrame.SPLIT_FRAME_TYPES.some((ft) => ft === this.frameId)) {
205✔
394
                // Some frames are designed to be split into multiple parts by a /
395
                fieldList.push(... value.split("/"));
1✔
396
            } else if (this.frameId === FrameIdentifiers.TCON) {
17✔
397
                // @TODO: Can we just make a separate class for TCON?
398
                // TCON in ID3v2.2 and ID3v2.3 is specified as
399
                // * (xx) - where xx is a number from the ID3v1 genre list
400
                // * (xx)yy - where xx is a number from the ID3v1 genre list and yyy is a
401
                //   "refinement" of the genre
402
                // * (RX) - "Remix"
403
                // * (CR) - "Cover"
404
                // * (( - used to escape a "(" in a refinement/genre name
405

406
                // NOTE: This encoding has an inherent flaw around how multiple genres should be
407
                //    encoded. Since multiple genres are already an edge case, I'm just going to
408
                //    say yolo to this whole block of code copied over from the .NET implementation
409
                while (value.length > 1 && value[0] === "(") {
8✔
410
                    const closing = value.indexOf(")");
24✔
411
                    if (closing < 0) {
24!
412
                        break;
×
413
                    }
414

415
                    const number = value.substring(1, closing);
24✔
416

417
                    let text: string;
418
                    if (number === TextInformationFrame.COVER_ABBREV) {
24✔
419
                        text = TextInformationFrame.COVER_STRING;
6✔
420
                    } else if (number === TextInformationFrame.REMIX_ABBREV) {
18✔
421
                        text = TextInformationFrame.REMIX_STRING;
6✔
422
                    } else {
423
                        text = Genres.indexToAudio(number, true);
12✔
424
                    }
425

426
                    if (!text) {
24!
427
                        // Number in parenthesis was not a numeric genre but part of a larger bit
428
                        // of text?
429
                        break;
×
430
                    }
431

432
                    // Number in parenthesis was a numeric genre
433
                    fieldList.push(text);
24✔
434
                    value = StringUtils.trimStart(value.substring(closing + 1), "/ ");
24✔
435

436
                    // Ignore genre if the same genre appears after the numeric genre
437
                    if (value.startsWith(text)) {
24✔
438
                        value = StringUtils.trimStart(value.substring(text.length), "/ ");
6✔
439
                    }
440
                }
441

442
                // Process whatever's left
443
                if (value.length > 0) {
8!
444
                    // Split the remaining genre value by dividers if the setting is turned on
445
                    let splitValue = Id3v2Settings.useNonStandardV2V3GenreSeparators
8✔
446
                        ? value.split(/[\/;]/).map((v) => v.trim()).filter((v) => !!v)
14✔
447
                        : [value];
448

449
                    splitValue = splitValue.map((v) => {
8✔
450
                        // Unescape escaped opening parenthesis
451
                        let v2 = v.replace(/\(\(/, "(");
14✔
452

453
                        // If non-standard numeric genres is enabled, parse them
454
                        if (Id3v2Settings.useNonStandardV2V3NumericGenres) {
14✔
455
                            const text = Genres.indexToAudio(v2, false);
13✔
456
                            if (text) {
13✔
457
                                v2 = text;
1✔
458
                            }
459
                        }
460

461
                        return v2;
14✔
462
                    });
463

464
                    fieldList.push(...splitValue);
8✔
465
                }
466
            } else {
467
                fieldList.push(value);
9✔
468
            }
469
        }
470

471
        // Bad tags may have one or more null characters at the end of a string, resulting in
472
        // empty strings at the end of the FieldList. Strip them off.
473
        while (fieldList.length !== 0
54✔
474
            && (!fieldList[fieldList.length - 1] || fieldList[fieldList.length - 1].length === 0)) {
475
            fieldList.splice(fieldList.length - 1, 1);
×
476
        }
477

478
        this._textFields = fieldList;
54✔
479
    }
480

481
    /** @inheritDoc */
482
    protected renderFields(version: number): ByteVector {
483
        if (this._rawData && this._rawVersion === version) {
210✔
484
            return this._rawData;
13✔
485
        }
486

487
        const encoding = TextInformationFrame.correctEncoding(this.textEncoding, version);
197✔
488
        const v = ByteVector.empty();
197✔
489
        let text = this._textFields;
197✔
490

491
        v.addByte(encoding);
197✔
492

493
        // Pre-process ID3v2.4 TCON frames
494
        if (version > 3 && this.frameId === FrameIdentifiers.TCON) {
197✔
495
            // For ID3v2.4, we should encode any genres that can be numeric as numeric by
496
            // themselves. This then gets encoded the same as any other ID3v2.4 text frame (ie,
497
            // with delimiters in between values)
498
            text = text.map((g) => {
8✔
499
                switch (g) {
12✔
500
                    case TextInformationFrame.COVER_STRING:
12✔
501
                        return TextInformationFrame.COVER_ABBREV;
2✔
502
                    case TextInformationFrame.REMIX_STRING:
503
                        return TextInformationFrame.REMIX_ABBREV;
2✔
504
                    default:
505
                        if (Id3v2Settings.useNumericGenres) {
8✔
506
                            const numericGenre = Genres.audioToIndex(g);
4✔
507
                            return numericGenre === 255 ? g : numericGenre.toString();
4✔
508
                        }
509
                        return g;
4✔
510
                }
511
            });
512
        }
513

514
        // Main processing
515
        const isTxxx = this.frameId === FrameIdentifiers.TXXX;
197✔
516
        if (version > 3 || isTxxx) {
197✔
517
            if (isTxxx) {
170✔
518
                if (text.length === 0) {
51✔
519
                    text = [null, null];
1✔
520
                } else if (text.length === 1) {
50✔
521
                    text = [text[0], null];
2✔
522
                }
523
            }
524

525
            for (let i = 0; i < text.length; i++) {
170✔
526
                // Since the field list is null delimited, if this is not the first element in the
527
                // list, append the appropriate delimiter for this encoding.
528
                if (i !== 0) {
219✔
529
                    v.addByteVector(ByteVector.getTextDelimiter(encoding));
64✔
530
                }
531

532
                if (text[i]) {
219✔
533
                    v.addByteVector(ByteVector.fromString(text[i], encoding));
215✔
534
                }
535
            }
536
        } else if (this.frameId === FrameIdentifiers.TCON) {
27✔
537
            // ID3v2.2 and ID3v2.3 TCON frames are going to be written with numeric genres first
538
            // (if enabled) and multiple text-based genres separated by ;.
539
            // NOTE: This doesn't follow the actual conventions for ID3v2.2/3 but nobody does this
540
            //    correctly. This implementation will at least work with MinimServer
541
            //    https://forum.minimserver.com/showthread.php?tid=2575
542
            const numericGenres = [];
2✔
543
            const textGenres = [];
2✔
544
            for (const s of text) {
2✔
545
                switch (s) {
12✔
546
                    case TextInformationFrame.COVER_STRING:
12✔
547
                        numericGenres.push(`(${TextInformationFrame.COVER_ABBREV})`);
2✔
548
                        break;
2✔
549
                    case TextInformationFrame.REMIX_STRING:
550
                        numericGenres.push(`(${TextInformationFrame.REMIX_ABBREV})`);
2✔
551
                        break;
2✔
552
                    default:
553
                        if (Id3v2Settings.useNumericGenres) {
8✔
554
                            const numericGenre = Genres.audioToIndex(s);
4✔
555
                            if (numericGenre !== 255) {
4✔
556
                                numericGenres.push(`(${numericGenre})`);
2✔
557
                                break;
2✔
558
                            }
559
                        }
560
                        textGenres.push(s.replace(/\(/, "(("));
6✔
561
                        break;
6✔
562
                }
563
            }
564

565
            // Put the entire string together
566
            const genreString = `${numericGenres.join("")}${textGenres.join(";")}`;
2✔
567
            v.addByteVector(ByteVector.fromString(genreString, encoding));
2✔
568
        } else {
569
            // Fields that have slashes in them and fields that don't
570
            v.addByteVector(ByteVector.fromString(text.join("/"), encoding));
25✔
571
        }
572

573
        return v;
197✔
574
    }
575

576
    // #endregion
577
}
578

579
export class UserTextInformationFrame extends TextInformationFrame {
1✔
580
    // #region Constructors
581

582
    private constructor(header: Id3v2FrameHeader) {
583
        super(header);
102✔
584
    }
585

586
    /**
587
     * Constructs and initializes a new instance with a specified description and text encoding.
588
     * @param description Description of the new frame
589
     * @param encoding Text encoding to use when rendering the new frame
590
     */
591
    public static fromDescription(
592
        description: string,
593
        encoding: StringType = Id3v2Settings.defaultEncoding
10✔
594
    ): UserTextInformationFrame {
595
        const frame = new UserTextInformationFrame(new Id3v2FrameHeader(FrameIdentifiers.TXXX));
73✔
596
        frame._encoding = encoding;
73✔
597
        frame.description = description;
73✔
598
        return frame;
73✔
599
    }
600

601
    /**
602
     * Constructs and initializes a new instance by reading its raw data in a specified ID3v2
603
     * version. This method allows for offset reading from the data byte vector.
604
     * @param data Raw representation of the new frame
605
     * @param offset What offset in `data` the frame actually begins. Must be positive,
606
     *     safe integer
607
     * @param header Header of the frame found at `data` in the data
608
     * @param version ID3v2 version the frame was originally encoded with
609
     */
610
    public static fromOffsetRawData(
611
        data: ByteVector,
612
        offset: number,
613
        header: Id3v2FrameHeader,
614
        version: number
615
    ): UserTextInformationFrame {
616
        Guards.truthy(data, "data");
33✔
617
        Guards.uint(offset, "offset");
31✔
618
        Guards.truthy(header, "header");
26✔
619
        Guards.byte(version, "version");
24✔
620

621
        const frame = new UserTextInformationFrame(header);
24✔
622
        frame.setData(data, offset, false, version);
24✔
623
        return frame;
24✔
624
    }
625

626
    /**
627
     * Constructs and initializes a new instance by reading its raw data in a specified
628
     * ID3v2 version.
629
     * @param data Raw representation of the new frame
630
     * @param version ID3v2 version the raw frame is encoded with, must be a positive 8-bit integer
631
     */
632
    public static fromRawData(data: ByteVector, version: number): UserTextInformationFrame {
633
        Guards.truthy(data, "data");
10✔
634
        Guards.byte(version, "version");
8✔
635

636
        const frame = new UserTextInformationFrame(Id3v2FrameHeader.fromData(data, version));
5✔
637
        frame.setData(data, 0, true, version);
5✔
638
        return frame;
5✔
639
    }
640

641
    // #endregion
642

643
    // #region Properties
644

645
    public get frameClassType(): FrameClassType { return FrameClassType.UserTextInformationFrame; }
184✔
646

647
    /**
648
     * Gets the description stored in the current instance.
649
     */
650
    public get description(): string {
651
        const text = super.text;
149✔
652
        return text.length > 0 ? text[0] : undefined;
149!
653
    }
654
    /**
655
     * Sets the description stored in the current instance.
656
     * There should only be one frame with the specified description per tag.
657
     * @param value Description to store in the current instance.
658
     */
659
    public set description(value: string) {
660
        let text = super.text;
74✔
661
        if (text.length > 0) {
74✔
662
            text[0] = value;
1✔
663
        } else {
664
            text = [ value ];
73✔
665
        }
666
        super.text = text;
74✔
667
    }
668

669
    /**
670
     * Gets the text contained in the current instance.
671
     * NOTE: Modifying the contents of the returned value will not modify the contents of the
672
     * current instance. The value must be reassigned for the value to change.
673
     */
674
    public get text(): string[] {
675
        const text = super.text;
57✔
676
        if (text.length < 2) {
57✔
677
            return [];
4✔
678
        }
679

680
        return text.slice(1);
53✔
681
    }
682
    /**
683
     * Sets the text contained in the current instance.
684
     * @param value Array of text values to store in the current instance
685
     */
686
    public set text(value: string[]) {
687
        const newValue = [this.description];
68✔
688
        newValue.push(... value);
68✔
689
        super.text = newValue;
68✔
690
    }
691

692
    // #endregion
693

694
    // #region Public Methods
695

696
    /**
697
     * Gets a user text information frame from a specified tag
698
     * @param frames Object to search in
699
     * @param description Description to use to match the frame in the `tag`
700
     * @param caseSensitive Whether or not to search for the frame case-sensitively.
701
     * @returns Frame containing the matching user, `undefined` if a match was not found
702
     */
703
    public static findUserTextInformationFrame(
704
        frames: UserTextInformationFrame[],
705
        description: string,
706
        caseSensitive: boolean = true
4✔
707
    ): UserTextInformationFrame {
708
        Guards.truthy(frames, "frames");
361✔
709

710
        const comparison = caseSensitive ? StringComparison.caseSensitive : StringComparison.caseInsensitive;
359✔
711
        return frames.find((f) => comparison(f.description, description));
359✔
712
    }
713

714
    /** @inheritDoc */
715
    public clone(): Frame {
716
        const frame = UserTextInformationFrame.fromDescription(undefined, this._encoding);
1✔
717
        frame._textFields = this._textFields.slice();
1✔
718
        if (this._rawData) {
1!
719
            frame._rawData = this._rawData.toByteVector();
1✔
720
        }
721
        frame._rawVersion = this._rawVersion;
1✔
722
        return frame;
1✔
723
    }
724

725
    /** @inheritDoc */
726
    public toString(): string {
727
        return `[${this.description}] ${super.toString()}`;
×
728
    }
729

730
    // #endregion
731
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc