1 |
|
2 | "use strict";
|
3 |
|
4 | const crypto = require('crypto');
|
5 |
|
6 | const mm = require('musicmetadata');
|
7 | const Mime = require('mime');
|
8 |
|
9 | const debug = require('debug')('upnpserver:contentHandlers:Musicmetadata');
|
10 |
|
11 | const logger = require('../logger');
|
12 | const ContentHandler = require('./contentHandler');
|
13 |
|
14 | class 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 |
|
43 |
|
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 |
|
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 |
|
242 | function 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 |
|
254 | function computeHash(buffer) {
|
255 | var shasum = crypto.createHash('sha1');
|
256 | shasum.update(buffer);
|
257 |
|
258 | return shasum.digest('hex');
|
259 | }
|
260 |
|
261 | module.exports = Audio_MusicMetadata;
|