UNPKG

5.87 kBJavaScriptView Raw
1/*jslint node: true, esversion: 6 */
2"use strict";
3
4const crypto = require('crypto');
5
6const mm = require('musicmetadata');
7const Mime = require('mime');
8
9const debug = require('debug')('upnpserver:contentHandlers:Musicmetadata');
10
11const logger = require('../logger');
12const ContentHandler = require('./contentHandler');
13
14class Audio_MusicMetadata extends ContentHandler {
15
16 /**
17 *
18 */
19 get name() {
20 return "musicMetadata";
21 }
22
23 /**
24 *
25 */
26 prepareMetas(contentInfos, context, callback) {
27
28 debug("Prepare", contentInfos);
29
30 var contentURL = contentInfos.contentURL;
31
32 contentURL.createReadStream(null, null, (error, stream) => {
33 if (error) {
34 return callback(error);
35 }
36
37 var parsing = true;
38
39 try {
40 debug("Start musicMetadata contentURL=", contentURL);
41 mm(stream, {
42 // duration : true
43 // fileSize : stats.size
44
45 }, (error, tags) => {
46
47 try {
48 stream.destroy();
49 } catch (x) {
50 logger.error("Can not close stream", x);
51 }
52
53 parsing = false;
54
55 if (error) {
56 logger.error("MM can not parse tags of contentURL=", contentURL, " error=",
57 error);
58 return callback();
59 }
60
61 debug("Parsed musicMetadata contentURL=", contentURL, "tags=", tags);
62
63 if (!tags) {
64 logger.error("MM does not support: " + contentURL);
65 return callback();
66 }
67
68 var metas = {};
69
70 ['title', 'album', 'duration'].forEach((n) => {
71 if (tags[n]) {
72 metas[n] = tags[n];
73 }
74 });
75
76 metas.albumArtists = normalize(tags.albumartist);
77 metas.artists = normalize(tags.artist);
78 metas.genres = normalize(tags.genre);
79
80 if (tags.year) {
81 metas.year = tags.year && parseInt(tags.year, 10);
82 }
83
84 var track = tags.track;
85 if (track) {
86 if (typeof (track.no) === "number" && track.no) {
87 metas.originalTrackNumber = track.no;
88
89 if (typeof (track.of) === "number" && track.of) {
90 metas.trackOf = track.of;
91 }
92 }
93 }
94
95 var disk = tags.disk;
96 if (disk) {
97 if (typeof (disk.no) === "number" && disk.no) {
98 metas.originalDiscNumber = disk.no;
99
100 if (typeof (disk.of) === "number" && disk.of) {
101 metas.diskOf = disk.of;
102 }
103 }
104 }
105
106 if (tags.picture) {
107 var as = [];
108 var res = [{}];
109
110 var index = 0;
111 tags.picture.forEach((picture) => {
112 var mimeType = Mime.lookup(picture.format);
113
114 var key = index++;
115
116 if (!mimeType) {
117 return;
118 }
119
120 if (!mimeType.indexOf("image/")) {
121
122 var hash = computeHash(picture.data);
123
124 as.push({
125 contentHandlerKey: this.name,
126 mimeType: mimeType,
127 size: picture.data.length,
128 hash: hash,
129 key: key
130 });
131 return;
132 }
133
134 res.push({
135 contentHandlerKey: this.name,
136 mimeType: mimeType,
137 size: picture.data.length,
138 key: key
139 });
140
141 });
142
143 if (as.length) {
144 metas.albumArts = as;
145 }
146 if (res.length > 1) {
147 metas.res = res;
148 }
149 }
150
151 callback(null, metas);
152 });
153 } catch (x) {
154 if (parsing) {
155 console.error("Catch ", x, x.stack);
156 try {
157 stream.destroy();
158
159 } catch (x) {
160 logger.error("Can not close stream", x);
161 }
162
163 logger.error("MM: Parsing exception contentURL=" + contentURL, x);
164 return callback();
165 }
166 logger.error("Catch ", x, x.stack);
167
168 throw x;
169 }
170 });
171 }
172
173 /**
174 *
175 */
176 processRequest(node, request, response, path, parameters, callback) {
177
178 var albumArtKey = parseInt(parameters[0], 10);
179 if (isNaN(albumArtKey) || albumArtKey < 0) {
180 let error = new Error("Invalid albumArtKey parameter (" + parameters + ")");
181 error.node = node;
182 error.request = request;
183
184 return callback(error, false);
185 }
186
187 var contentURL = node.contentURL;
188 // console.log("Get stream of " + node, node.attributes);
189
190 this._getPicture(node, contentURL, albumArtKey, (error, picture) => {
191
192 if (!picture.format || !picture.data) {
193 let error = new Error('Invalid picture for node #' + node.id + ' key=' + albumArtKey);
194 error.node = node;
195 error.request = request;
196
197 return callback(error, false);
198 }
199
200 response.setHeader("Content-Type", picture.format);
201 response.setHeader("Content-Size", picture.data.length);
202
203 response.end(picture.data, () => callback(null, true));
204 });
205 }
206
207 _getPicture(node, contentURL, pictureIndex, callback) {
208
209 contentURL.createReadStream(null, null, (error, stream) => {
210 if (error) {
211 return callback(error);
212 }
213
214 mm(stream, (error, tags) => {
215 try {
216 stream.destroy();
217 } catch (x) {
218 logger.error("Can not close stream", x);
219 }
220
221 if (error) {
222 logger.error("Can not parse ID3 of " + contentURL, error);
223 return callback("Can not parse ID3");
224 }
225
226 if (!tags || !tags.picture || tags.picture.length <= pictureIndex) {
227 let error = new Error('Picture #' + pictureIndex + ' not found');
228
229 logger.error(error);
230 return callback(error);
231 }
232
233 var picture = tags.picture[pictureIndex];
234 tags = null;
235
236 callback(null, picture);
237 });
238 });
239 }
240}
241
242function normalize(strs) {
243 var r = [];
244 if (!strs || !strs.length) {
245 return undefined;
246 }
247 strs.forEach((str) => str.split(',').forEach(
248 (tok) =>
249 r.push(tok.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()).trim())
250 ));
251 return r;
252}
253
254function computeHash(buffer) {
255 var shasum = crypto.createHash('sha1');
256 shasum.update(buffer);
257
258 return shasum.digest('hex');
259}
260
261module.exports = Audio_MusicMetadata;