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

Raiper34 / m3u-parser-generator / f006c20f-d420-4c28-9078-525e45377361

15 Dec 2023 04:06PM UTC coverage: 93.789% (-4.3%) from 98.089%
f006c20f-d420-4c28-9078-525e45377361

push

circleci

Raiper34
fix(parser): change attibutes parser to be able to handle strings starting with space (attribute=" value"), also skip invalid attributes by default

48 of 54 branches covered (0.0%)

Branch coverage included in aggregate %.

5 of 6 new or added lines in 1 file covered. (83.33%)

2 existing lines in 1 file now uncovered.

103 of 107 relevant lines covered (96.26%)

13.93 hits per line

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

90.63
/src/m3u-parser.ts
1
import {
1✔
2
  M3uPlaylist,
3
  M3uMedia,
4
  M3uAttributes,
5
  M3uDirectives,
6
  M3U_COMMENT
7
} from "./m3u-playlist";
8

9
/**
10
 * M3u parser class to parse m3u playlist string to playlist object
11
 */
12
export class M3uParser {
1✔
13

14
  /**
15
   * Get m3u attributes object from attributes string
16
   * @param attributesString e.g. 'tvg-id="" group-title=""'
17
   * @param ignoreErrors ignore errors and try to
18
   * @returns attributes object e.g. {"tvg-id": "", "group-title": ""}
19
   * @private
20
   */
21
  private static getAttributes(attributesString: string, ignoreErrors: boolean): M3uAttributes {
22
    const attributes: M3uAttributes = new M3uAttributes();
14✔
23
    if (!attributesString) {
14✔
24
      return attributes;
3✔
25
    }
26
    try {
11✔
27
      const attributeValuePair = attributesString.match(/[^ ]*?=".*?"/g) ?? []; // regex to find `attribute="value"`
11!
28
      attributeValuePair.forEach((item) => {
11✔
29
        const [key, value] = item.split('="');
57✔
30
        if (value == null) {
57!
NEW
31
          throw new Error(`Attribute value can't be null!`);
×
32
        }
33
        attributes[key] = value.replace('"', '');
57✔
34
      });
35
    } catch (error: unknown) {
UNCOV
36
      if (!ignoreErrors) {
×
UNCOV
37
        throw new Error((error as {message: string}).message)
×
38
      }
39
    }
40
    return attributes;
11✔
41
  }
42

43
  /**
44
   * Process media method parse trackInformation and fill media with parsed info
45
   * @param trackInformation - media substring of m3u string line e.g. '-1 tvg-id="" group-title="",Tv Name'
46
   * @param media - actual m3u media object
47
   * @param ignoreErrors - ignore errors in file and try to parse it with it
48
   * @private
49
   */
50
  private static processMedia(trackInformation: string, media: M3uMedia, ignoreErrors: boolean): void {
51
    const lastCommaIndex = trackInformation.lastIndexOf(',');
14✔
52
    const durationAttributes = trackInformation.substring(0, lastCommaIndex);
14✔
53
    media.name = trackInformation.substring(lastCommaIndex + 1);
14✔
54

55
    const firstSpaceIndex = durationAttributes.indexOf(' ');
14✔
56
    const durationEndIndex = firstSpaceIndex > 0 ? firstSpaceIndex : durationAttributes.length;
14✔
57
    media.duration = Number(durationAttributes.substring(0, durationEndIndex));
14✔
58
    const attributes = durationAttributes.substring(durationEndIndex + 1);
14✔
59

60
    media.attributes = this.getAttributes(attributes, ignoreErrors);
14✔
61
  }
62

63
  /**
64
   * Process directive method detects directive on line and call proper method to another processing
65
   * @param item - actual line of m3u playlist string e.g. '#EXTINF:-1 tvg-id="" group-title="",Tv Name'
66
   * @param playlist - m3u playlist object processed until now
67
   * @param media - actual m3u media object
68
   * @param ignoreErrors - ignore errors in file and try to parse it with it
69
   * @private
70
   */
71
  private static processDirective(item: string, playlist: M3uPlaylist, media: M3uMedia, ignoreErrors: boolean): void {
72
    const firstSemicolonIndex = item.indexOf(':');
42✔
73
    const directive = item.substring(0, firstSemicolonIndex);
42✔
74
    const trackInformation = item.substring(firstSemicolonIndex + 1);
42✔
75
    switch(directive) {
42✔
76
      case M3uDirectives.EXTINF: {
77
        this.processMedia(trackInformation, media, ignoreErrors);
14✔
78
        break;
14✔
79
      }
80
      case M3uDirectives.EXTGRP: {
81
        media.group = trackInformation;
13✔
82
        break;
13✔
83
      }
84
      case M3uDirectives.PLAYLIST: {
85
        playlist.title = trackInformation;
1✔
86
        break;
1✔
87
      }
88
      case M3uDirectives.EXTATTRFROMURL: {
89
        media.extraAttributesFromUrl = trackInformation;
1✔
90
        break;
1✔
91
      }
92
      case M3uDirectives.EXTHTTP: {
93
        media.extraHttpHeaders = JSON.parse(trackInformation);
1✔
94
        break;
1✔
95
      }
96
      case M3uDirectives.KODIPROP: {
97
        const [key, value] = trackInformation.split('=');
3✔
98

99
        if(!media.kodiProps) {
3!
100
          media.kodiProps = new Map<string, string>();
×
101
        }
102

103
        media.kodiProps.set(key, value);
3✔
104
        break;
3✔
105
      }
106
    }
107
  }
108

109
  /**
110
   * Process directive method detects directive on line and call proper method to another processing
111
   * @param item - actual line of m3u playlist string e.g. '#EXTINF:-1 tvg-id="" group-title="",Tv Name'
112
   * @param playlist - m3u playlist object processed until now
113
   * @private
114
   */
115
  private static processUrlTvg(item: string, playlist: M3uPlaylist): void {
116
    const urlTvgValue = item.split('url-tvg="')[1];
10✔
117
    if (urlTvgValue) {
9✔
118
      playlist.urlTvg = urlTvgValue.split('"')[0];
1✔
119
    }
120
  }
121

122
  /**
123
   * Get playlist returns m3u playlist object parsed from m3u string lines
124
   * @param lines - m3u string lines
125
   * @param ignoreErrors - ignore errors in file and try to parse it with it
126
   * @returns parsed m3u playlist object
127
   * @private
128
   */
129
  private static getPlaylist(lines: string[], ignoreErrors: boolean): M3uPlaylist {
130
    const playlist = new M3uPlaylist();
10✔
131
    let media = new M3uMedia('');
10✔
132

133
    this.processUrlTvg(lines[0], playlist);
10✔
134

135
    lines.forEach(item => {
9✔
136
      if (this.isDirective(item)) {
57✔
137
        this.processDirective(item, playlist, media, ignoreErrors);
42✔
138
      } else {
139
        media.location = item;
15✔
140
        playlist.medias.push(media);
15✔
141
        media = new M3uMedia('');
15✔
142
      }
143
    });
144
    return playlist;
9✔
145
  }
146

147
  /**
148
   * Is directive method detect if line contains m3u directive
149
   * @param item - string line of playlist
150
   * @returns true if it is line with directive, otherwise false
151
   * @private
152
   */
153
  private static isDirective(item: string): boolean {
154
    return item[0] === M3U_COMMENT;
57✔
155
  }
156

157
  /**
158
   * Is valid m3u method detect if first line of playlist contains #EXTM3U directive
159
   * @param firstLine - first line of m3u playlist string
160
   * @returns true if line starts with #EXTM3U, false otherwise
161
   * @private
162
   */
163
  private static isValidM3u(firstLine: string[]): boolean {
164
    return firstLine[0].startsWith(M3uDirectives.EXTM3U);
9✔
165
  }
166

167
  /**
168
   * Parse is static method to parse m3u playlist string into m3u playlist object.
169
   * Playlist need to contain #EXTM3U directive on first line.
170
   * All lines are trimmed and blank ones are removed.
171
   * @param m3uString - whole m3u playlist string
172
   * @param ignoreErrors - ignore errors in file and try to parse it with it
173
   * @returns parsed m3u playlist object
174
   * @example
175
   * ```ts
176
   * const playlist = M3uParser.parse(m3uString);
177
   * playlist.medias.forEach(media => media.location);
178
   * ```
179
   */
180
  static parse(m3uString: string, ignoreErrors = false): M3uPlaylist {
10✔
181
    if (!ignoreErrors && !m3uString) {
12✔
182
      throw new Error(`m3uString can't be null!`);
1✔
183
    }
184

185
    const lines = m3uString.split('\n').map(item => item.trim()).filter(item => item != '');
65✔
186

187
    if (!ignoreErrors && !this.isValidM3u(lines)) {
11✔
188
      throw new Error(`Missing ${M3uDirectives.EXTM3U} directive!`);
1✔
189
    }
190
    return this.getPlaylist(lines, ignoreErrors);
10✔
191
  }
192
}
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

© 2025 Coveralls, Inc