MusicBrainz function library

Musicbrainz function library.

目前为 2014-09-26 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/5140/18912/MusicBrainz%20function%20library.js

  1. // ==UserScript==
  2. // @name MusicBrainz function library
  3. // @namespace http://www.jens-bertram.net/userscripts/mbz-lib
  4. // @description Musicbrainz function library.
  5. // @require https://code.jquery.com/jquery-2.1.1.min.js
  6. // @version 0.2.0beta
  7. // @grant none
  8. // @supportURL https://github.com/JensBee/userscripts
  9. // @license MIT
  10. // ==/UserScript==
  11. window.MBZ = {};
  12.  
  13. MBZ.baseUrl = 'https://musicbrainz.org/';
  14. MBZ.iconUrl = MBZ.baseUrl + 'favicon.ico',
  15.  
  16. MBZ.Html = {
  17. /**
  18. * Add CSS entry to pages <head/>.
  19. * @param style definition to add
  20. */
  21. _addGlobalStyle: function(css) {
  22. if ($('head').length == 0) {
  23. $('body').append($('<head>'));
  24. }
  25. var style = $('head>style');
  26. if (style.length == 0) {
  27. style = $('<style>');
  28. style.attr('type', 'text/css');
  29. $('head').append(style);
  30. }
  31. style.append(css);
  32. },
  33.  
  34. _init: function() {
  35. this._addGlobalStyle(
  36. 'button.mbzButton {cursor:pointer;' +
  37. 'text-decoration:none; text-shadow:-1px -1px 0 rgba(255,201,97,0.3); font-weight:bold; color:#000;' +
  38. 'padding:5px 5px 5px 25px; border-radius:5px;' +
  39. 'border-top:1px solid #736CAE; border-left:1px solid #736CAE;' +
  40. 'border-bottom:1px solid #FFC961; border-right:1px solid #FFC961;' +
  41. 'background:#FFE3B0 url("' + MBZ.iconUrl + '") no-repeat 5px center;}' +
  42. 'button.mbzButton:hover {' +
  43. 'border:1px solid #454074; background-color:#FFD88C;}' +
  44. 'button.mbzButton:disabled {cursor:default;' +
  45. 'border:1px solid #ccc; background-color:#ccc; color:#5a5a5a;}'
  46. );
  47. },
  48.  
  49. /**
  50. * Create a MusicBrainz link.
  51. * @params[type] type to link to (e.g. release)
  52. * @params[id] mbid to link to (optional)
  53. * @params[more] stuff to add after mbid + '/' (optional)
  54. * @return plain link text
  55. */
  56. getLink: function (params) {
  57. return MBZ.baseUrl + params.type + '/' + (params.id ? params.id + '/' : '') + (params.more || '');
  58. },
  59.  
  60. /**
  61. * Create a MusicBrainz link.
  62. * @params[type] type to link to (e.g. release)
  63. * @params[id] mbid to link to (optional)
  64. * @params[more] stuff to add after mbid + '/' (optional)
  65. * @params[title] link title attribute (optional)
  66. * @params[text] link text (optional)
  67. * @params[before] stuff to put before link (optional)
  68. * @params[after] stuff to put after link (optional)
  69. * @params[icon] true/false: include MusicBrainz icon (optional, default: true)
  70. * @return link jQuery object
  71. */
  72. getLinkElement: function (params) {
  73. params.icon = (typeof params.icon !== 'undefined' && params.icon == false ? false : true);
  74. var retEl = $('<div style="display:inline-block;">');
  75. if (params.before) {
  76. retEl.append(params.before);
  77. }
  78. var linkEl = $('<a>' + (params.icon ? this.mbzIcon : '') + (params.text || '') + '</a>');
  79. linkEl.attr('href', this.getLink({
  80. type: params.type,
  81. id: params.id,
  82. more: params.more
  83. })).attr('target', '_blank');
  84. if (params.title) {
  85. linkEl.attr('title', params.title);
  86. }
  87. retEl.append(linkEl);
  88. if (params.after) {
  89. retEl.append(params.after);
  90. }
  91. return retEl;
  92. },
  93.  
  94. getMbzButton: function(caption, title) {
  95. var btn = $('<button type="button" class="mbzButton">' + caption + '</button>');
  96. if (title) {
  97. btn.attr('title', title);
  98. }
  99. return btn;
  100. },
  101.  
  102. mbzIcon: '<img src="' + MBZ.iconUrl + '" />'
  103. };
  104. MBZ.Html._init();
  105.  
  106. MBZ.Util = {
  107. /**
  108. * Convert anything to string.
  109. * @data object
  110. */
  111. asString: function (data) {
  112. if (data == null) {
  113. return '';
  114. }
  115. switch (typeof data) {
  116. case 'string':
  117. return data.trim();
  118. case 'object':
  119. return data.toString().trim();
  120. case 'function':
  121. return 'function';
  122. case 'undefined':
  123. return '';
  124. default:
  125. data = data + '';
  126. return data.trim();
  127. }
  128. },
  129.  
  130. /**
  131. * Creates http + https url from a given https? url.
  132. * @url http/https url
  133. * @return array with given url prefixed with http + https or single url, if not https? protocol
  134. */
  135. expandProtocol: function(url) {
  136. var urls;
  137. if (url.toLowerCase().startsWith('http')) {
  138. var urlPath = url.replace(/^https?:\/\//,'');
  139. urls = ['http://' + urlPath, 'https://' + urlPath];
  140. } else {
  141. urls = [url];
  142. }
  143. return urls;
  144. },
  145.  
  146. /**
  147. * Creates http + https urls from a given array of https? urls.
  148. * @urls array of http/https urls
  149. * @return array with given urls prefixed with http + https
  150. */
  151. expandProtocols: function(urls) {
  152. var newUrls = [];
  153. var self = this;
  154. $.each(urls, function(idx, val){
  155. newUrls = newUrls.concat(self.expandProtocol(val));
  156. });
  157. return newUrls;
  158. },
  159.  
  160. /**
  161. * Convert HH:MM:SS, MM:SS, SS to seconds.
  162. * http://stackoverflow.com/a/9640417
  163. * @str string
  164. * @return seconds extracted from initial string
  165. */
  166. hmsToSeconds: function (str) {
  167. str = MBZ.Util.asString(str);
  168. if (str.indexOf(':') > -1) {
  169. var p = str.split(':'), s = 0, m = 1;
  170.  
  171. while (p.length > 0) {
  172. s += m * parseInt(p.pop(), 10);
  173. m *= 60;
  174. }
  175.  
  176. return s;
  177. } else {
  178. return str;
  179. }
  180. },
  181.  
  182. /**
  183. * Remove a trailing slash from a string
  184. * @str string
  185. * @return intial string with trailing slash removed
  186. */
  187. rmTrSlash: function (str) {
  188. if(str.substr(-1) == '/') {
  189. return str.substr(0, str.length - 1);
  190. }
  191. return str;
  192. }
  193. };
  194.  
  195. MBZ.CA = {
  196. baseUrl: 'https://coverartarchive.org/',
  197. // no https here (bad_cert notice)
  198. originBaseUrl: 'https://cors-anywhere.herokuapp.com/coverartarchive.org:443/',
  199.  
  200. /**
  201. * Create a CoverArtArchive link.
  202. * @params[type] type to link to (e.g. release)
  203. * @params[id] mbid to link to (optional)
  204. * @params[more] stuff to add after mbid (optional)
  205. */
  206. getLink: function (params) {
  207. return this.originBaseUrl + params.type + '/' + (params.id ? params.id + '/' : '') + (params.more || '');
  208. },
  209. };
  210.  
  211. /**
  212. * MusicBrainz web service v2 interface.
  213. */
  214. MBZ.WS = {
  215. _baseUrl: MBZ.baseUrl + 'ws/2/',
  216. _queue: [],
  217. _pollFreq: 1100,
  218. _pollInterval: null,
  219.  
  220. /**
  221. * Add to request queue.
  222. * @params[cb] callback
  223. * @params[url] request url
  224. * @params[args] callback function parameters object
  225. * @params[scope] scope for calling callback function
  226. */
  227. _qAdd: function(params) {
  228. this._queue.push(params);
  229. if (!this._pollInterval) {
  230. if (this._queue.length == 1) {
  231. this._qPoll();
  232. }
  233. this._pollInterval = setInterval(this._qPoll, this._pollFreq);
  234. }
  235. },
  236.  
  237. /**
  238. * Execute queued requests.
  239. */
  240. _qPoll: function() {
  241. if (MBZ.WS._queue.length > 0) {
  242. var item = MBZ.WS._queue.pop();
  243. $.getJSON(item.url, function(data) {
  244. if (item.args) {
  245. if (item.scope) {
  246. item.cb.call(item.scope, data, item.args);
  247. } else {
  248. item.cb(data, item.args);
  249. }
  250. } else {
  251. if (item.scope) {
  252. item.cb.call(item.scope, data);
  253. } else {
  254. item.cb(data);
  255. }
  256. }
  257. }).fail(function(jqxhr, textStatus, error) {
  258. var err = textStatus + ', ' + error;
  259. console.error("Request (" + item.url + ") failed: " + err);
  260. if (item.scope) {
  261. item.cb.call(item.scope);
  262. } else {
  263. item.cb();
  264. }
  265. });
  266. } else if (MBZ.WS._queue.length == 0 && MBZ.WS._pollInterval) {
  267. clearInterval(MBZ.WS._pollInterval);
  268. }
  269. },
  270.  
  271. /**
  272. * Lookup a musicbrainz url relation
  273. * @params[cb] callback function
  274. * @params[res] url to lookup
  275. * @params[rel] relation type
  276. * @params[scope] scope for callback function
  277. */
  278. getUrlRelation: function (params) {
  279. this._qAdd({
  280. cb: params.cb,
  281. url: this._baseUrl + 'url?resource=' + encodeURIComponent(params.res) + '&inc=' + params.rel + '-rels',
  282. scope: params.scope
  283. });
  284. },
  285.  
  286. /**
  287. * Lookup musicbrainz url relations
  288. * @params[urls] array of urls to lookup
  289. * @params[rel] relation type
  290. * @params[cb] callback function for each response
  291. * @params[cbInc] callback for each item looked up
  292. * @params[cbDone] callback to call if all items have been looked up
  293. * @params[scope] scope for callback functions
  294. */
  295. getUrlRelations: function(params) {
  296. var self = this;
  297. var count = params.urls.length;
  298. var current = 0;
  299. function localCb(data) {
  300. if (params.scope) {
  301. params.cb.call(params.scope, data);
  302. } else {
  303. params.cb(data);
  304. }
  305. if (typeof params.cbInc === 'function') {
  306. if (params.scope) {
  307. params.cbInc.call(params.scope);
  308. } else {
  309. params.cbInc();
  310. }
  311. }
  312. if (++current == count && typeof params.cbDone === 'function') {
  313. if (params.scope) {
  314. params.cbDone.call(params.scope);
  315. } else {
  316. params.cbDone();
  317. }
  318. }
  319. }
  320. $.each(params.urls, function(idx, val) {
  321. self.getUrlRelation({
  322. cb: localCb,
  323. res: val,
  324. rel: params.rel
  325. });
  326. });
  327. }
  328. };
  329.  
  330. /**
  331. * Release related functions.
  332. */
  333. MBZ.Release = function() {
  334. var form = $('<form method="post" id="' + MBZ.Release._form.baseName + '-' + (MBZ.Release._form.count++) +
  335. '" target="_blank" action="' + MBZ.Release._form.target + '" acceptCharset="UTF-8"></form>');
  336.  
  337. this.data = {
  338. annotation: '', // content
  339. artists: [],
  340. labels: [],
  341. mediums: [],
  342. note: '', // content
  343. packaging: '', // type
  344. releases: [],
  345. title: '', // content
  346. tracks: [],
  347. urls: [] // [target, type]
  348. };
  349.  
  350. function addField(name, value) {
  351. name = MBZ.Util.asString(name);
  352. value = MBZ.Util.asString(value);
  353. if (name.length > 0 && value.length > 0) {
  354. form.append($('<input type="hidden" name="' + name + '" value="' + value
  355. .replace(/&/g, '&amp;')
  356. .replace(/"/g, '&quot;')
  357. .replace(/'/g, '&#39;')
  358. .replace(/</g, '&lt;')
  359. .replace(/>/g, '&gt;')
  360. + '"/>'));
  361. }
  362. }
  363.  
  364. function buildForm(dataSet) {
  365. if (dataSet.annotation != '') {
  366. addField('annotation', dataSet.annotation);
  367. }
  368.  
  369. if (dataSet.artists.length > 0) {
  370. $.each(dataSet.artists, function(idx, val) {
  371. var prefix = 'artist_credit.names.' + (val.idx || idx);
  372. addField(prefix + '.name', val.cred);
  373. addField(prefix + '.mbid', val.id);
  374. addField(prefix + '.artist.name', val.name);
  375. addField(prefix + '.join_phrase', val.join);
  376. });
  377. }
  378.  
  379. if (dataSet.labels.length > 0) {
  380. $.each(dataSet.labels, function(idx, val) {
  381. var prefix = 'labels.' + (val.idx || idx);
  382. addField(prefix + '.mbid', val.id);
  383. addField(prefix + '.name', val.name);
  384. addField(prefix + '.catalog_number', val.catNo);
  385. });
  386. }
  387.  
  388. if (dataSet.note != '') {
  389. addField('edit_note', dataSet.note);
  390. }
  391.  
  392. if (dataSet.releases.length > 0) {
  393. $.each(dataSet.releases, function(idx, val) {
  394. var prefix = 'events.' + (val.idx || idx);
  395. addField(prefix + '.date.year', val.y);
  396. addField(prefix + '.date.month', val.m);
  397. addField(prefix + '.date.day', val.d);
  398. addField(prefix + '.country', val.cc);
  399. });
  400. }
  401.  
  402. $.each(dataSet.mediums, function(idx, val) {
  403. var prefix = 'mediums.' + (val.idx || idx);
  404. addField(prefix + '.format', val.fmt);
  405. addField(prefix + '.name', val.name);
  406. });
  407.  
  408. if (dataSet.packaging != '') {
  409. addField('packaging', dataSet.packaging);
  410. }
  411.  
  412. if (dataSet.title != '') {
  413. addField('name', dataSet.title);
  414. }
  415.  
  416. $.each(dataSet.tracks, function(idx, val) {
  417. var prefix = 'mediums.' + val.med + '.track.' + (val.idx || idx);
  418. addField(prefix + '.name', val.tit);
  419. addField(prefix + '.number', val.num);
  420. addField(prefix + '.recording', val.recId);
  421. addField(prefix + '.length', val.dur);
  422.  
  423. if (val.artists) {
  424. $.each(val.artists, function(aIdx, aVal) {
  425. var aPrefix = prefix + '.artist_credit.names.' + (aVal.idx || aIdx);
  426. addField(aPrefix + '.name', aVal.cred);
  427. addField(aPrefix + '.mbid', aVal.id);
  428. addField(aPrefix + '.artist.name', aVal.name);
  429. addField(aPrefix + '.join_phrase', aVal.join);
  430. });
  431. }
  432. });
  433.  
  434. if (dataSet.urls.length > 0) {
  435. $.each(dataSet.urls, function(idx, val) {
  436. addField('urls.' + idx + '.url', val[0]);
  437. addField('urls.' + idx + '.link_type', val[1]);
  438. });
  439. }
  440. }
  441.  
  442. /**
  443. * Submit data to musicbrainz.
  444. */
  445. this.submitRelease = function() {
  446. buildForm(this.data);
  447. $('body').append(form);
  448. form.submit();
  449. };
  450. };
  451.  
  452. MBZ.Release._relationCb = function(data) {
  453. if (!data) {
  454. return {};
  455. }
  456. if (data.relations) {
  457. var rels = {_res: data.resource};
  458. $.each(data.relations, function(idx, val) {
  459. var id = val.release.id;
  460. var type = val.type;
  461. if (!rels[id]) {
  462. rels[id] = [];
  463. }
  464. if (rels[id].indexOf(type) == -1) {
  465. rels[id].push(type);
  466. }
  467. });
  468. return rels;
  469. }
  470. };
  471.  
  472. MBZ.Release._form = {
  473. baseName: 'mbAddReleaseForm',
  474. count: 0,
  475. target: MBZ.baseUrl + 'release/add'
  476. };
  477.  
  478. /**
  479. * Lookup a musicbrainz url relation for 'release' type.
  480. * @params[cb] callback function
  481. * @params[res] url to lookup
  482. * @params[scope] scope for callback function
  483. */
  484. MBZ.Release.getUrlRelation = function(params) {
  485. function innerCb(cbData) {
  486. if (params.scope) {
  487. params.cb.call(params.scope, MBZ.Release._relationCb(cbData));
  488. } else {
  489. params.cb(MBZ.Release._relationCb(cbData));
  490. }
  491. }
  492. MBZ.WS.getUrlRelation({
  493. cb: innerCb,
  494. res: params.res,
  495. rel: 'release',
  496. scope: params.scope
  497. });
  498. };
  499.  
  500. /**
  501. * Lookup musicbrainz url relations for 'release' type.
  502. * @params[urls] array of urls to lookup
  503. * @params[cb] callback function for each response
  504. * @params[cbInc] callback for each item looked up
  505. * @params[cbDone] callback to call if all items have been looked up
  506. * @params[scope] scope for callback functions
  507. */
  508. MBZ.Release.getUrlRelations = function(params) {
  509. function innerCb(cbData) {
  510. if (params.scope) {
  511. params.cb.call(params.scope, MBZ.Release._relationCb(cbData));
  512. } else {
  513. params.cb(MBZ.Release._relationCb(cbData));
  514. }
  515. }
  516. MBZ.WS.getUrlRelations({
  517. urls: params.urls,
  518. rel: 'release',
  519. cb: innerCb,
  520. cbInc: params.cbInc,
  521. cbDone: params.cbDone,
  522. scope: params.scope
  523. });
  524. };
  525.  
  526. MBZ.Release.prototype = {
  527. /**
  528. * Add an artist entry.
  529. * @params plain artist name as string or object:
  530. * params[cred] artist name as credited
  531. * params[id] artists mbid
  532. * params[idx] position
  533. * params[join] phrase to join with next artist
  534. * params[name] artist name
  535. */
  536. addArtist: function(params) {
  537. if (typeof params === 'string') {
  538. this.data.artists.push({name: params});
  539. } else {
  540. this.data.artists.push(params);
  541. }
  542. },
  543.  
  544. /**
  545. * Add a label entry.
  546. * @params plain label name as string or object.
  547. * params[catNo] catalog number
  548. * params[id] mbid
  549. * params[idx] position
  550. * params[name] label name
  551. */
  552. addLabel: function(params) {
  553. if (typeof params === 'string') {
  554. this.data.labels.push({name: params});
  555. } else {
  556. this.data.labels.push(params);
  557. }
  558. },
  559.  
  560. /**
  561. * Set format of a medium.
  562. * @params[idx] position
  563. * @params[fmt] format type name
  564. * @params[name] name
  565. */
  566. addMedium: function(params) {
  567. this.data.mediums.push(params)
  568. },
  569.  
  570. /**
  571. * Add a release event.
  572. * @params[y] YYYY
  573. * @params[m] MM
  574. * @params[d] DD
  575. * @params[cc] country code
  576. * @params[idx] position
  577. */
  578. addRelease: function(params) {
  579. this.data.releases.push(params);
  580. },
  581.  
  582. /**
  583. * Add a track.
  584. * @params[med] medium number
  585. * @params[tit] track name
  586. * @params[idx] track number
  587. * @params[num] track number (free-form)
  588. * @params[dur] length in MM:SS or milliseconds
  589. * @params[recId] mbid of existing recording to associate
  590. * @params[artists] array of objects:
  591. * obj[cred] artist name as credited
  592. * obj[id] artists mbid
  593. * obj[idx] position
  594. * obj[join] phrase to join with next artist
  595. * obj[name] artist name
  596. */
  597. addTrack: function(params) {
  598. this.data.tracks.push(params);
  599. },
  600.  
  601. /**
  602. * @url target url
  603. * @type musicbrainz url type
  604. * @return true if value was added
  605. */
  606. addUrl: function(url, type) {
  607. url = MBZ.Util.asString(url);
  608. type = MBZ.Util.asString(type);
  609.  
  610. this.data.urls.push([url, type]);
  611. return true;
  612. },
  613.  
  614. /**
  615. * Dump current data (best viewed in FireBug).
  616. */
  617. dump: function() {
  618. console.log(this.data);
  619. },
  620.  
  621. /**
  622. * @content annotation content
  623. * @return old value
  624. */
  625. setAnnotation: function(content) {
  626. var old = this.data.annotation;
  627. this.data.annotation = MBZ.Util.asString(content);
  628. return old;
  629. },
  630.  
  631. /**
  632. * @content edeting note content
  633. * @return old value
  634. */
  635. setNote: function(content) {
  636. var old = this.data.note;
  637. this.data.note = MBZ.Util.asString(content);
  638. return old;
  639. },
  640.  
  641. /**
  642. * @content packaging type
  643. * @return old value
  644. */
  645. setPackaging: function(type) {
  646. var old = this.data.packaging;
  647. this.data.packaging = MBZ.Util.asString(type);
  648. return old;
  649. },
  650.  
  651. /**
  652. * @name release title
  653. * @return old value
  654. */
  655. setTitle: function(name) {
  656. var old = this.data.title;
  657. this.data.title = MBZ.Util.asString(name);
  658. return old;
  659. },
  660. };