@pixwel/pixwel-sdk 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.babelrc +3 -0
  2. package/.editorconfig +36 -0
  3. package/.vs/slnx.sqlite +0 -0
  4. package/.vscode/launch.json +0 -0
  5. package/coverage/clover.xml +7 -0
  6. package/coverage/coverage-final.json +1 -0
  7. package/coverage/lcov-report/Ass.js.html +651 -0
  8. package/coverage/lcov-report/Asset.js.html +129 -0
  9. package/coverage/lcov-report/Filename.js.html +849 -0
  10. package/coverage/lcov-report/Ingest.js.html +591 -0
  11. package/coverage/lcov-report/IngestFile.js.html +87 -0
  12. package/coverage/lcov-report/IngestFileDrop.js.html +540 -0
  13. package/coverage/lcov-report/Order.js.html +162 -0
  14. package/coverage/lcov-report/Srt.js.html +333 -0
  15. package/coverage/lcov-report/Sub.js.html +177 -0
  16. package/coverage/lcov-report/TimeFormat.js.html +210 -0
  17. package/coverage/lcov-report/base.css +223 -0
  18. package/coverage/lcov-report/block-navigation.js +63 -0
  19. package/coverage/lcov-report/index.html +84 -0
  20. package/coverage/lcov-report/lib/Filename.js.html +720 -0
  21. package/coverage/lcov-report/lib/index.html +97 -0
  22. package/coverage/lcov-report/prettify.css +1 -0
  23. package/coverage/lcov-report/prettify.js +1 -0
  24. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  25. package/coverage/lcov-report/sorter.js +158 -0
  26. package/coverage/lcov-report/test/index.html +97 -0
  27. package/coverage/lcov-report/test/mocks.js.html +423 -0
  28. package/coverage/lcov.info +0 -0
  29. package/dist/index.js +61 -0
  30. package/dist/src/Ass.js +200 -0
  31. package/dist/src/Asset.js +41 -0
  32. package/dist/src/Filename.js +252 -0
  33. package/dist/src/Ingest.js +200 -0
  34. package/dist/src/IngestFile.js +17 -0
  35. package/dist/src/IngestFileDrop.js +178 -0
  36. package/dist/src/Order.js +58 -0
  37. package/dist/src/Srt.js +102 -0
  38. package/dist/src/Sub.js +42 -0
  39. package/dist/src/TimeFormat.js +48 -0
  40. package/index.js +6 -0
  41. package/package.json +26 -0
  42. package/src/Ass.js +195 -0
  43. package/src/Asset.js +20 -0
  44. package/src/Filename.js +260 -0
  45. package/src/Ingest.js +175 -0
  46. package/src/IngestFile.js +6 -0
  47. package/src/IngestFileDrop.js +157 -0
  48. package/src/Order.js +31 -0
  49. package/src/Srt.js +89 -0
  50. package/src/Sub.js +37 -0
  51. package/src/TimeFormat.js +48 -0
  52. package/src/docs.json +1 -0
  53. package/test/Ass.spec.js +150 -0
  54. package/test/Asset.spec.js +24 -0
  55. package/test/Filename.spec.js +728 -0
  56. package/test/Ingest.spec.js +95 -0
  57. package/test/Srt.spec.js +111 -0
  58. package/test/Sub.spec.js +75 -0
  59. package/test/TimeFormat.spec.js +84 -0
  60. package/test/mocks/mediainfo.mock.js +67 -0
  61. package/test/mocks/mediainfo.png.mock.js +22 -0
  62. package/test/mocks/mocks.js +118 -0
  63. package/test/mocks/prores.mock.js +71 -0
  64. package/test/parse.spec.ts +927 -0
  65. package/tsconfig.json +11 -0
@@ -0,0 +1,260 @@
1
+ import _ from "lodash";
2
+
3
+ /**
4
+ * The Filename class parses a filename into tokens, and can also take a set of
5
+ * tokens and create a valid Pixwel filename.
6
+ * @type class
7
+ */
8
+ export default class Filename {
9
+ /**
10
+ * Renders a valid filename given all tags, all types, an asset, and metadata
11
+ * options likely parsed from the file itself.
12
+ * @param {Array} [allTags=[]] Collection of all known tags
13
+ * @param {Array} [allTypes=[]] Collection of all known types
14
+ * @param {Object} [asset={}] A Platform Asset
15
+ * @param {Object} [options={}] options containing metadata about file
16
+ * @param {String} [mediaType=''] Indicates video, audio, image and allows for stricter required fields
17
+ * @return {String} A valid filename
18
+ */
19
+ static render(
20
+ allTags = [],
21
+ allTypes = [],
22
+ asset = {},
23
+ options = {},
24
+ mediaType = ""
25
+ ) {
26
+ if (!asset.project) {
27
+ throw "missing asset.project";
28
+ }
29
+
30
+ if (asset.project && !asset.project.filePrefix) {
31
+ throw "missing asset.project.filePrefix";
32
+ }
33
+
34
+ if (!asset.name) {
35
+ throw "missing asset.name";
36
+ }
37
+
38
+ if (!asset.type) {
39
+ throw "missing asset.type";
40
+ }
41
+
42
+ if (asset.type.match(/^film-clip|^featurette/) && !asset.length) {
43
+ throw "missing asset.length";
44
+ }
45
+
46
+ let defaults = {
47
+ language: null,
48
+ country: null,
49
+ version: null,
50
+ tags: {
51
+ Usage: null,
52
+ Aspect: null,
53
+ SubDub: {
54
+ sub: null,
55
+ dub: null
56
+ },
57
+ Resolution: null,
58
+ "Frame Mode": null,
59
+ Framerate: null,
60
+ Text: null,
61
+ Codec: null,
62
+ Extra: []
63
+ }
64
+ };
65
+ options = _.merge(defaults, options);
66
+
67
+ if (!options.language) {
68
+ throw "missing options.language";
69
+ }
70
+
71
+ if (
72
+ !options.tags.SubDub.sub &&
73
+ !options.tags.SubDub.dub &&
74
+ !options.tags.Text
75
+ ) {
76
+ throw "missing options.tags.Text or options.tags.SubDub";
77
+ }
78
+
79
+ if (mediaType && mediaType == "video" && !options.tags.Codec) {
80
+ throw "missing options.tags.Codec";
81
+ }
82
+
83
+ if (mediaType && mediaType == "video" && !options.tags.Resolution) {
84
+ throw "missing options.tags.Resolution";
85
+ }
86
+
87
+ if (mediaType && mediaType == "video" && !options.tags.Framerate) {
88
+ throw "missing options.tags.Framerate";
89
+ }
90
+
91
+ if (mediaType == "video" && !options.tags["Frame Mode"]) {
92
+ throw "missing options.tags[Frame Mode]";
93
+ }
94
+
95
+ // currently, slurpee's filename.ts will render this, but we can add this back in later when
96
+ // filename.ts starts to rely on Filename.render
97
+ // if ((options.tags.SubDub.dub || options.tags.SubDub.sub) && options.tags.Text) {
98
+ // throw 'conflicting options supplied: SubDub and Text';
99
+ // }
100
+
101
+ function assetType(asset, types) {
102
+ if (/trailer/.test(asset.type)) {
103
+ return false;
104
+ }
105
+
106
+ let matchingType = _.find(types, { name: asset.type });
107
+ if (!matchingType) {
108
+ throw `Asset type "${asset.type}" could not be found`;
109
+ }
110
+
111
+ return (
112
+ matchingType.file || matchingType.name.replace(".", "-").toUpperCase()
113
+ );
114
+ }
115
+
116
+ function assetTitle(asset) {
117
+ let prefix = "";
118
+ let name = asset.slug.toUpperCase().replace("INT-L", "INTL");
119
+ if (/trailer/.test(asset.type)) {
120
+ switch (asset.type) {
121
+ case "trailer.intl":
122
+ prefix = "ITR-";
123
+ name = name.replace(/INTL-TRAILER-/, "");
124
+ break;
125
+ case "trailer":
126
+ prefix = "TR-";
127
+ name = name.replace(/TRAILER-/, "");
128
+ break;
129
+ case "trailer.us":
130
+ prefix = "DTR-";
131
+ name = name.replace(/DOMESTIC-TRAILER-/, "");
132
+ break;
133
+ }
134
+ }
135
+ return prefix + name;
136
+ }
137
+
138
+ function runningTime(asset) {
139
+ let matches = asset.type.match(/^film-clip|^featurette/);
140
+ if (!matches) {
141
+ return;
142
+ }
143
+
144
+ let minutes = Math.floor(asset.length / 60);
145
+ let seconds = asset.length % 60;
146
+
147
+ let result = "";
148
+
149
+ if (minutes > 0) {
150
+ result += minutes + "min";
151
+ }
152
+
153
+ if (seconds > 0) {
154
+ result += seconds + "sec";
155
+ }
156
+
157
+ return result;
158
+ }
159
+
160
+ function locale(country, language) {
161
+ return [language, country]
162
+ .filter(str => {
163
+ return !!str && str !== "WW";
164
+ })
165
+ .join("-");
166
+ }
167
+
168
+ function textType(subDub, text) {
169
+ if (!subDub || (!subDub.sub && !subDub.dub)) {
170
+ return text;
171
+ }
172
+
173
+ let result = [];
174
+ if (subDub.sub) {
175
+ result.push("sub");
176
+ }
177
+
178
+ if (subDub.dub) {
179
+ result.push("dub");
180
+ }
181
+
182
+ return result;
183
+ }
184
+
185
+ function matchSet(allTags, tags) {
186
+ let selectedTags = _(tags)
187
+ .values()
188
+ .flatten()
189
+ .value();
190
+
191
+ let setTag = _(allTags)
192
+ .filter("set")
193
+ .find(setTag => {
194
+ return _.difference(setTag.set, selectedTags).length === 0;
195
+ });
196
+
197
+ if (setTag) {
198
+ return setTag.name;
199
+ }
200
+ }
201
+
202
+ let tags = options.tags,
203
+ parts = [
204
+ asset.project.filePrefix,
205
+ assetType(asset, allTypes),
206
+ assetTitle(asset),
207
+ runningTime(asset),
208
+ locale(options.country, options.language),
209
+ textType(tags.SubDub, tags.Text),
210
+ tags.Extra.join("_"),
211
+ options.version,
212
+ tags.Codec
213
+ ];
214
+
215
+ parts = _(parts)
216
+ .flatten()
217
+ .filter()
218
+ .map(part => part.toUpperCase());
219
+
220
+ let matchedSet = matchSet(allTags, options.tags);
221
+ let requiredInfo = tags.Resolution && tags["Frame Mode"] && tags.Framerate;
222
+
223
+ if (matchedSet) {
224
+ parts = parts.push(matchedSet);
225
+ } else if (mediaType == "video" && tags.Resolution && !requiredInfo) {
226
+ parts = parts.push(tags.Resolution.replace(/\d+x/, ""));
227
+ } else if (mediaType == "image" && tags.Resolution) {
228
+ parts = parts.push(tags.Resolution);
229
+ }
230
+
231
+ let name = parts.join("_");
232
+
233
+ let hasResInfo = name.match(/\d+(i|p)/i);
234
+ let hasAllInfo = name.match(/\d+(i|p)\d+/i);
235
+
236
+ if (requiredInfo && !hasResInfo && !hasAllInfo) {
237
+ let res = tags.Resolution;
238
+ let encodeTag = [
239
+ hasResInfo ? "" : res.indexOf("x") ? res.replace(/\d+x/, "") : res,
240
+ res.match(/i|p/i) && !hasResInfo
241
+ ? ""
242
+ : tags["Frame Mode"].toLowerCase().slice(0, 1),
243
+ tags.Framerate.replace(".", "").toUpperCase()
244
+ ]
245
+ .filter(val => {
246
+ return val;
247
+ })
248
+ .join("");
249
+
250
+ let match = new RegExp(encodeTag, "i");
251
+ name = name.match(match)
252
+ ? name.replace(match, encodeTag)
253
+ : name + "_" + encodeTag;
254
+ } else if (hasResInfo && !hasAllInfo && tags.Framerate) {
255
+ name += tags.Framerate.replace(".", "").toUpperCase();
256
+ }
257
+
258
+ return name;
259
+ }
260
+ }
package/src/Ingest.js ADDED
@@ -0,0 +1,175 @@
1
+ import _ from "lodash";
2
+
3
+ /**
4
+ * [media description]
5
+ * @type {[type]}
6
+ */
7
+ export default class Ingest {
8
+ /**
9
+ * [mapMetaToTags description]
10
+ * @param {[type]} info [description]
11
+ * @return {[type]} [description]
12
+ */
13
+ static mapMetaToTags(info, mediaType) {
14
+
15
+ let tags = {
16
+ Aspect: "",
17
+ Standard: "",
18
+ Resolution: "",
19
+ Codec: "",
20
+ "Frame Mode": "",
21
+ Text: "",
22
+ Usage: "",
23
+ Extra: [],
24
+ Framerate: "",
25
+ Duration: ""
26
+ };
27
+
28
+ switch (mediaType) {
29
+ case 'video':
30
+ mediaType = 'Video';
31
+ break;
32
+ case 'image':
33
+ mediaType = 'Image';
34
+ break;
35
+ default:
36
+ return tags;
37
+ }
38
+
39
+ if (!info.File) {
40
+ throw "missing info.File";
41
+ }
42
+
43
+ if (!info.File.track) {
44
+ throw "missing info.File.track";
45
+ }
46
+
47
+ let mediainfoDefaults = {
48
+ Display_aspect_ratio: "",
49
+ Format: "",
50
+ Standard: "",
51
+ Height: "",
52
+ Width: "",
53
+ Frame_rate: "",
54
+ Scan_type: ""
55
+ };
56
+
57
+ let media = _.find(info["File"]["track"], {
58
+ _type: mediaType
59
+ });
60
+ media = _.merge({}, mediainfoDefaults, media);
61
+
62
+ let width = parseInt(media["Width"].replace(" ", "").match(/[0-9 ]+/));
63
+ let height = parseInt(media["Height"].replace(" ", "").match(/[0-9 ]+/));
64
+ tags.Resolution = width && height ? `${width}x${height}` : "";
65
+
66
+ tags.Standard = media["Standard"] ? media["Standard"].toLowerCase() : "";
67
+
68
+ let scan = /Progressive/.test(media["Scan_type"]) ? "p" : "i";
69
+ let dimension = /3D/.test(name) ? "3D" : "2D";
70
+
71
+ let framerate = media["Frame_rate"].split(" ");
72
+ if (framerate.length) {
73
+ let f = framerate[0];
74
+ if (f == "23.976") {
75
+ f = "23.98";
76
+ }
77
+ f = parseFloat(f).toString();
78
+ tags.Framerate = f != "NaN" ? f : "";
79
+ }
80
+
81
+ if (!tags.Standard) {
82
+ switch (tags.Framerate) {
83
+ case "24":
84
+ case "23.98":
85
+ case "29.97":
86
+ case "59.94":
87
+ tags.Standard = "ntsc";
88
+ break;
89
+ case "25":
90
+ case "50":
91
+ tags.Standard = "pal";
92
+ break;
93
+ }
94
+ }
95
+
96
+ tags.Codec = (function(codec) {
97
+ switch (codec) {
98
+ case "AVC":
99
+ return "h264";
100
+ case "ProRes":
101
+ return "prores";
102
+ default:
103
+ return "";
104
+ }
105
+ })(media["Format"]);
106
+
107
+ if (tags.Codec == "h264") {
108
+ tags.Usage = "online";
109
+ }
110
+
111
+ if (tags.Codec == "prores") {
112
+ tags.Usage = "broadcast";
113
+ }
114
+
115
+ tags.Aspect = media["Display_aspect_ratio"] || "";
116
+ tags["Frame Mode"] = media["Scan_type"].toLowerCase();
117
+
118
+ return tags;
119
+ }
120
+ static mapFromKnownData(context, order, tags, watermarks = []) {
121
+
122
+ if (order.asset) {
123
+ context = {
124
+ asset: order.asset._id,
125
+ project: order.project._id,
126
+ assetType: order.assetType,
127
+ purpose: "download"
128
+ };
129
+ }
130
+
131
+ let previewType = (function() {
132
+ if (order.isSub() && order.isDub()) return "both";
133
+ if (order.isDub()) return "dub";
134
+ if (order.isSub()) return "sub";
135
+ })();
136
+
137
+ let isPreviewEnabled =
138
+ tags.Codec == "prores" &&
139
+ tags.Resolution.split("x")[1] == "1080" &&
140
+ (tags.Framerate == "23.98" || tags.Framerate == "25") &&
141
+ watermarks.length;
142
+
143
+ let watermark = watermarks[0] ? watermarks[0]._id : "";
144
+
145
+ return {
146
+ workRequest: order._id,
147
+ selected: {
148
+ download: {
149
+ enabled: true,
150
+ country: order.country,
151
+ language: order.language,
152
+ tags: tags,
153
+ embargo: {
154
+ enabled: false
155
+ }
156
+ },
157
+ preview: {
158
+ enabled: isPreviewEnabled,
159
+ language: order.language,
160
+ revision: "Final",
161
+ type: previewType,
162
+ watermark: watermark,
163
+ embargo: {
164
+ enabled: false
165
+ }
166
+ },
167
+ thumbnail: {
168
+ enabled: false
169
+ }
170
+ },
171
+ context: context
172
+ };
173
+ }
174
+ }
175
+
@@ -0,0 +1,6 @@
1
+ export default class IngestFile {
2
+ constructor(file, order) {
3
+ this.file = file;
4
+ }
5
+ // renderedName, ingest, options
6
+ }
@@ -0,0 +1,157 @@
1
+ import { Asset, Filename, Order, Ingest, IngestFile } from "@pixwel/pixwel-sdk";
2
+
3
+ /**
4
+ * Collects file handles from HTML5, full paths from Aspera, inspects
5
+ * the dropped file using mediainfo, then renames the file according to
6
+ * the data from Mediainfo and the Order and submits it for upload.
7
+ * @type {Object}
8
+ */
9
+ export default class IngestFileDrop {
10
+ /**
11
+ * Map that contains data from HTML5 file drop and Aspera file drop
12
+ * @type {Object}
13
+ */
14
+ constructor(scope, Mediainfo, IngestUpload) {
15
+ this.map = {};
16
+ this.scope = scope;
17
+ this.IngestUpload = IngestUpload;
18
+ this.Mediainfo = Mediainfo;
19
+ this.asperaDropHandler = this.asperaDropHandler.bind(this);
20
+ this.htmlDropHandler = this.htmlDropHandler.bind(this);
21
+ }
22
+ /**
23
+ * HTML file drop handler
24
+ * @param {[type]} e [description]
25
+ * @return {[type]} [description]
26
+ */
27
+ htmlDropHandler(e) {
28
+ let files = e.dataTransfer.files;
29
+ for (var i = 0; i < files.length; i++) {
30
+ this.registerHtmlFile(files[i]);
31
+ }
32
+ }
33
+ /**
34
+ * Aspera drop handler
35
+ * @param {[type]} data [description]
36
+ * @return {[type]} [description]
37
+ */
38
+ asperaDropHandler(data) {
39
+ var that = this;
40
+ let files = data.files.dataTransfer.files;
41
+ if (!files.length) {
42
+ return;
43
+ }
44
+ for (var i = 0; i < files.length; i++) {
45
+ this.registerAsperaFile(files[i]);
46
+ }
47
+ return Promise.all(this.export()).then(renamed => {
48
+ that.IngestUpload.uploadFiles(renamed, {}).then(() => {
49
+ that.map = {};
50
+ });
51
+ });
52
+ }
53
+ /**
54
+ * Registers and HTML file drop
55
+ * @param {[type]} file [description]
56
+ * @return {[type]} [description]
57
+ */
58
+ registerHtmlFile(file) {
59
+ this.map[file.name] = file;
60
+ }
61
+ /**
62
+ * Registers and Aspera file drop
63
+ * @param {[type]} file [description]
64
+ * @return {[type]} [description]
65
+ */
66
+ registerAsperaFile(file) {
67
+ let basename = this.getBasename(file);
68
+ this.map[basename]["path"] = file.name;
69
+ }
70
+ /**
71
+ * Gets correct basename for MacOS and Windows
72
+ * @param {[type]} file [description]
73
+ * @return {[type]} [description]
74
+ */
75
+ getBasename(file) {
76
+ return file.name[0] === "/"
77
+ ? file.name.split("/").pop()
78
+ : file.name.split("\\").pop();
79
+ }
80
+ /**
81
+ * Renames and generates ingest record for internal filemap
82
+ * @return {[type]} [description]
83
+ */
84
+ export() {
85
+ var that = this;
86
+ return Object.values(this.map).map(file => {
87
+ return that.rename(file);
88
+ });
89
+ }
90
+ /**
91
+ * Renames the file and decorates it with ingest data using data from
92
+ * mediainfo and the Order
93
+ * @param {[type]} file [description]
94
+ * @return {[type]} [description]
95
+ */
96
+ rename(file) {
97
+ var that = this;
98
+ return this.Mediainfo.inform(file).then(meta => {
99
+ let order;
100
+ let asset;
101
+ let mediaType = file.type.split("/")[0];
102
+
103
+ let tags = Ingest.mapMetaToTags(meta, mediaType);
104
+
105
+ if (!that.scope.workRequest) {
106
+ order = new Order();
107
+ asset = new Asset();
108
+ tags.Text = "txtd";
109
+ file.ingest = Ingest.mapFromKnownData(
110
+ that.scope.context,
111
+ order,
112
+ tags,
113
+ that.scope.watermarks
114
+ );
115
+ return file;
116
+ }
117
+
118
+ order = new Order(that.scope.workRequest);
119
+ asset = Asset.mapFromOrder(order);
120
+
121
+ let options = {
122
+ tags: tags,
123
+ country: order.country,
124
+ language: order.language
125
+ };
126
+
127
+ if (that.scope.request && that.scope.request.tag) {
128
+ options.tags.Extra = [that.scope.request.tag.replace(" ", "-")];
129
+ }
130
+
131
+ options.tags.SubDub = {
132
+ sub: order.isSub(),
133
+ dub: order.isDub()
134
+ };
135
+ if (!order.isSub() && !order.isDub()) {
136
+ options.tags.Text = "txtd";
137
+ }
138
+
139
+ let basename = Filename.render(
140
+ that.scope.tags,
141
+ that.scope.assetTypes,
142
+ asset,
143
+ options,
144
+ mediaType
145
+ );
146
+ let extension = file.name.split(".").pop();
147
+ file.renderedName = `${basename}.${extension}`;
148
+ file.ingest = Ingest.mapFromKnownData(
149
+ that.scope.context,
150
+ order,
151
+ tags,
152
+ that.scope.watermarks
153
+ );
154
+ return file;
155
+ });
156
+ }
157
+ }
package/src/Order.js ADDED
@@ -0,0 +1,31 @@
1
+ import _ from "lodash";
2
+ import Filename from "./Filename.js";
3
+
4
+ export default class Order {
5
+ constructor(data) {
6
+ let schema = {
7
+ _id: null,
8
+ asset: null,
9
+ project: null,
10
+ assetType: null,
11
+ country: null,
12
+ language: null,
13
+ dng: null
14
+ };
15
+ Object.assign(this, schema, data);
16
+ }
17
+ isDub() {
18
+ if (!this.dng) {
19
+ return null;
20
+ }
21
+ return _
22
+ .flattenDeep(Object.entries(this.dng))
23
+ .includes("Dedicated / Localized");
24
+ }
25
+ isSub() {
26
+ if (!this.dng) {
27
+ return null;
28
+ }
29
+ return _.flattenDeep(Object.entries(this.dng)).includes("Subtitled");
30
+ }
31
+ }
package/src/Srt.js ADDED
@@ -0,0 +1,89 @@
1
+ import _ from 'lodash';
2
+ import TimeFormat from './TimeFormat';
3
+
4
+ function stripTags(input, allowed) {
5
+ allowed = (((allowed || '') + '').toLowerCase().match(/<[a-z][a-z0-9]*>/g) || []).join('')
6
+ var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi
7
+ var commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi
8
+ return input.replace(commentsAndPhpTags, '').replace(tags, function($0, $1) {
9
+ return allowed.indexOf('<' + $1.toLowerCase() + '>') > -1 ? $0 : ''
10
+ })
11
+ }
12
+
13
+ var SRT_STATE_SUBNUMBER = 0,
14
+ SRT_STATE_TIME = 1,
15
+ SRT_STATE_TEXT = 2,
16
+ SRT_STATE_BLANK = 3;
17
+
18
+ export default {
19
+ parse: function(lines) {
20
+ var result = [],
21
+ state = SRT_STATE_SUBNUMBER,
22
+ subText = '',
23
+ subTime = '';
24
+
25
+ function time(subTime) {
26
+
27
+ var range = subTime.split(' --> ');
28
+ if (!TimeFormat.isValidTime(range[0]) || !TimeFormat.isValidTime(range[1])) {
29
+ throw new Error('Malformed SRT file');
30
+ }
31
+
32
+ return {
33
+ startTime: range[0],
34
+ stopTime: range[1]
35
+ };
36
+ }
37
+
38
+ lines.forEach(function(line) {
39
+ line = line.replace(/^\s+|\s+$/g, '');
40
+
41
+ switch (state) {
42
+ case SRT_STATE_SUBNUMBER:
43
+ state = SRT_STATE_TIME;
44
+ break;
45
+
46
+ case SRT_STATE_TIME:
47
+ subTime = line;
48
+ state = SRT_STATE_TEXT;
49
+ break;
50
+
51
+ case SRT_STATE_TEXT:
52
+ if (line !== '') {
53
+ subText += (subText) ? "\n" + line : line;
54
+ return;
55
+ }
56
+ result.push(Object.assign(time(subTime), {
57
+ text: subText
58
+ }));
59
+ subText = '';
60
+ state = SRT_STATE_SUBNUMBER;
61
+ break;
62
+ }
63
+ });
64
+
65
+ return result;
66
+ },
67
+ export: function(lines) {
68
+ var result = '',
69
+ i = 1;
70
+
71
+ lines.forEach(function(line) {
72
+ if (line.text === undefined) {
73
+ return;
74
+ }
75
+
76
+ // Replace <br> tags with newlines. These tags may accidentally be stored in the translation
77
+ let text = _.unescape(line.text.replace(/<br>/g, '\n'));
78
+
79
+ // Remove any tags that may have made it into the translation (div tags seem to be the main problem).
80
+ // <b> <i> are the only two supported tags currently
81
+ text = stripTags(text, '<b><i>');
82
+
83
+ result += (i++) + "\n";
84
+ result += TimeFormat.startTime(line) + " --> " + TimeFormat.stopTime(line) + "\n";
85
+ result += text + "\n\n";
86
+ });
87
+ return result;
88
+ }
89
+ };