MusicBrainz: Archive.org importer

Import audio files and collections into Musicbrainz. Also supports scanning bookmarks and search results for MusicBrainz relations.

  1. // ==UserScript==
  2. // @name MusicBrainz: Archive.org importer
  3. // @namespace http://www.jens-bertram.net/userscripts/import-internetarchive
  4. // @description Import audio files and collections into Musicbrainz. Also supports scanning bookmarks and search results for MusicBrainz relations.
  5. // @icon http://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Internet_Archive_logo_and_wordmark.png/240px-Internet_Archive_logo_and_wordmark.png
  6. // @supportURL https://github.com/JensBee/userscripts
  7. // @license MIT
  8. // @version 0.4.7beta
  9. //
  10. // @grant none
  11. // @require https://code.jquery.com/jquery-2.1.1.min.js
  12. // @require https://greasyfork.org/scripts/5140-musicbrainz-function-library/code/MusicBrainz%20function%20library.js?version=21997
  13. //
  14. // @include *://archive.org/details/*
  15. // @include *://archive.org/bookmarks.php
  16. // @include *//archive.org/search.php*
  17. // ==/UserScript==
  18. var mbz = mbz || {};
  19. mbz.archive_org_importer = {
  20. // https://archive.org/about/faqs.php#Audio
  21. audioFormats: [
  22. 'mp3',
  23. 'flac',
  24. 'ogg',
  25. 'audio',
  26. 'aiff',
  27. 'shorten',
  28. 'weba'],
  29.  
  30. /**
  31. * Check file type for audio format. Filters out most (but not all) other
  32. * file types.
  33. * @formatStr file format name
  34. */
  35. isAudioFile: function(formatStr) {
  36. formatStr = formatStr.toLowerCase();
  37. for (var format in this.audioFormats) {
  38. if (formatStr.contains(format)) {
  39. return true;
  40. }
  41. }
  42. return false;
  43. }
  44. };
  45.  
  46. /**
  47. * Functions to parse a list of links for MusicBrainz relations.
  48. */
  49. mbz.archive_org_importer.linkCheck = {
  50. btn: MBZ.Html.getMbzButton('Check link relations',
  51. 'Check entries being linked from MusicBrainz.'),
  52.  
  53. /**
  54. * Link scanner status values
  55. */
  56. links: {
  57. found: null,
  58. checked: 0,
  59. matched: 0
  60. },
  61.  
  62. /**
  63. * RexEx to strip off current base-url.
  64. */
  65. re: new RegExp('^'+window.location.origin),
  66.  
  67. /**
  68. * Scan status elements.
  69. */
  70. status: {
  71. base: $('<span>'),
  72. current: $('<span>'),
  73. matched: $('<span>')
  74. },
  75.  
  76. /**
  77. * Start scanner.
  78. * @params[links] jQuery object with target links
  79. * @params[controlEl] jQuery element to append controls to
  80. */
  81. scan: function(params) {
  82. this.links.found = params.links;
  83.  
  84. if (this.links.found.length > 0 && params.controlEl) {
  85. var self = this;
  86.  
  87. this.status.current.text(this.links.checked);
  88. this.status.matched.text(this.links.matched);
  89. this.status.base.append('&nbsp;Checked: ')
  90. .append(this.status.current)
  91. .append('&nbsp;Matches: ')
  92. .append(this.status.matched)
  93. .hide();
  94.  
  95. this.btn.click(function () {
  96. self.btn.prop("disabled", true);
  97. self.btn.text("Checking..");
  98. self.status.base.show();
  99. var urls = [];
  100. $.each(self.links.found, function(idx, link) {
  101. urls.push('http://archive.org'+$(link).attr('href'));
  102. });
  103. MBZ.Release.getUrlRelations({
  104. urls: MBZ.Util.expandProtocols(urls),
  105. cb: self.rel.attach,
  106. cbInc: self.rel.inc,
  107. cbDone: self.rel.done,
  108. scope: self
  109. });
  110. });
  111. params.controlEl.append(this.btn).append(this.status.base);
  112. }
  113. },
  114.  
  115. /**
  116. * Callback handlers for relation parsing.
  117. */
  118. rel: {
  119. /**
  120. * Relation was found, data is attached.
  121. */
  122. attach: function(data) {
  123. if (!data._res) {
  124. return;
  125. }
  126. var res = data._res.replace(this.re, '');
  127. var self = this;
  128. $.each(self.links.found, function(idx, link) {
  129. var link = $(link);
  130. if (link.attr('href') == res) {
  131. self.status.matched.text(self.links.matched++);
  132. MBZ.Release.insertMBLink(data, link);
  133. }
  134. });
  135. },
  136.  
  137. /**
  138. * All relations have been resolved.
  139. */
  140. done: function() {
  141. this.status.base.html('&nbsp;' + this.links.checked
  142. + ' links checked with ' + this.links.matched + ' matches.');
  143. this.btn.text('Check done');
  144. },
  145.  
  146. /**
  147. * A relation was checked.
  148. */
  149. inc: function() {
  150. this.status.current.text(this.links.checked++);
  151. },
  152. }
  153. }
  154.  
  155. /**
  156. * Functions to import a single release.
  157. */
  158. mbz.archive_org_importer.Release = function() {
  159. this.btn = MBZ.Html.getMbzButton('Import',
  160. 'Import this release to MusicBrainz');
  161. this.dEl = $('<div id="mbzDialog">').hide(); // dialog elements
  162. this.mbLinkTarget = null;
  163. this.importRunning = false;
  164. this.importInitialized = false;
  165. // release data object finally passed on to MusicBrainz.
  166. this.release = new MBZ.Release();
  167. this.tracks = new mbz.archive_org_importer.Release.Tracks();
  168. var self = this;
  169. var submitted = false;
  170.  
  171. /**
  172. * Initialize release parsing.
  173. */
  174. function init() {
  175. this.tracks.detectSources();
  176.  
  177. var playerJSON = this.tracks.getPlayerJSON();
  178. if (playerJSON.length == 0) {
  179. console.error('Player JSON data not found. Disabling MusicBrainz import.');
  180. return;
  181. }
  182.  
  183. var cEl = $('<div id="mbzControls">'); // control elements
  184. var url = MBZ.Util.rmTrSlash($(location).attr('href'));
  185. var urlJSON = url + '&output=json';
  186. var trackData = $.parseJSON(playerJSON);
  187. var pageJSON = null; // page data as JSON object
  188.  
  189. this.btn.click(function () {
  190. if (submitted) {
  191. self.release.submitRelease();
  192. return;
  193. }
  194.  
  195. if (!self.importInitialized) {
  196. self.btn.prop("disabled", true);
  197. self.btn.text("Initializing import");
  198. // prepare source data
  199. $.getJSON(urlJSON, function (data) {
  200. pageJSON = data;
  201. self.tracks.parseSources.call(self, data);
  202. }).fail(function(jqxhr, textStatus, error) {
  203. var err = textStatus + ', ' + error;
  204. console.error("Request (" + urlJSON + ") failed: " + err);
  205. self.btn.text("ERROR");
  206. });
  207. return;
  208. }
  209.  
  210. self.dEl.hide();
  211. self.btn.prop("disabled", true);
  212. // *** static data
  213. self.release.addMedium({
  214. idx: 0,
  215. fmt: 'Digital Media'
  216. });
  217. self.release.setPackaging('none');
  218. self.release.setNote('Imported from The Internet Archive (' + url + ')');
  219. // *** parsed data from release JSON object
  220. self.parseJSON.urls(self.release, pageJSON);
  221. self.parseJSON.artists(self.release, pageJSON);
  222. self.parseJSON.title(self.release, pageJSON);
  223. self.parseJSON.labels(self.release, pageJSON);
  224. self.parseJSON.release(self.release, pageJSON);
  225. self.parseJSON.annotation(self.release, pageJSON);
  226. self.tracks.commit(self.release);
  227. // submit
  228. //self.release.dump();
  229. self.btn.text("Submitting..");
  230. self.release.submitRelease(function() {
  231. submitted = true;
  232. self.btn.prop("disabled", false);
  233. self.btn.text("Submit again");
  234. });
  235. });
  236. $('.breadcrumbs').before(cEl.append(this.btn));
  237. cEl.after(self.dEl);
  238. self.mbLinkTarget = self.btn;
  239. MBZ.Release.getUrlRelations({
  240. urls: MBZ.Util.expandProtocol(url),
  241. cb: MBZ.Release.insertMBLink,
  242. scope: self
  243. });
  244. };
  245.  
  246. init.call(this);
  247. };
  248. mbz.archive_org_importer.Release.prototype = {
  249. /**
  250. * Callback function. Called when all sources are parsed.
  251. */
  252. enableImport: function() {
  253. this.importInitialized = true;
  254.  
  255. if (this.tracks.validSources > 1) {
  256. this.tracks.showSources.call(this);
  257. this.btn.text("Start import");
  258. this.btn.prop("disabled", false);
  259. } else {
  260. this.btn.click();
  261. }
  262. }
  263. };
  264. /**
  265. * Parse JSON response for a release.
  266. */
  267. mbz.archive_org_importer.Release.prototype.parseJSON = {
  268. annotation: function (release, data) {
  269. if (data.metadata.notes) {
  270. release.setAnnotation(data.metadata.notes[0]);
  271. }
  272. },
  273. artists: function (release, data) {
  274. if (data.metadata.creator) {
  275. $.each(data.metadata.creator, function (idx, val) {
  276. release.addArtist(val);
  277. });
  278. }
  279. },
  280. labels: function (release, data) {
  281. if (data.metadata.collection) {
  282. $.each(data.metadata.collection, function (idx, val) {
  283. release.addLabel({
  284. name: val,
  285. catNo: data.metadata.identifier[0]
  286. });
  287. });
  288. }
  289. },
  290. release: function (releaseObj, data) {
  291. var dates = data.metadata.date || data.metadata.publicdate;
  292. if (dates) {
  293. $.each(dates, function (idx, val) {
  294. var date = val.match(/([0-9]{4})-([0-9]{2})-([0-9]{2}).*/);
  295. if (date && date.length == 4) {
  296. releaseObj.addRelease({
  297. y: date[1],
  298. m: date[2],
  299. d: date[3],
  300. cc:'XW'
  301. });
  302. }
  303. });
  304. }
  305. },
  306. urls: function (release, data) {
  307. var url = $(location).attr('href');
  308. release.addUrl(url, '75');
  309. release.addUrl(url, '85');
  310. if (data.creativecommons && data.creativecommons.license_url) {
  311. release.addUrl(data.creativecommons.license_url, '301');
  312. }
  313. },
  314. title: function (release, data) {
  315. if (data.metadata.title) {
  316. release.setTitle(data.metadata.title[0]);
  317. }
  318. },
  319. /**
  320. * First parse track list from player JSON data. The provided information
  321. * may not be complete, so gather the parsed data in a local array.
  322. */
  323. tracksFromPlayer: function(data) {
  324. if (data.length > 0) {
  325. var self = this;
  326. $.each(data, function(idx, val) {
  327. var duration = MBZ.Util.hmsToSeconds(val.duration);
  328. duration = Math.round(parseFloat(duration) * 1000); // sec to msec
  329. if (isNaN(duration)) {
  330. duration = null;
  331. }
  332. // get source file name
  333. var file = val.sources[0].file;
  334. if (file) {
  335. self.tracks.updateData({
  336. med: 0,
  337. tit: val.title.replace(/^[0-9]+\.\s/,''),
  338. idx: idx,
  339. dur: duration,
  340. file: MBZ.Util.getLastPathSegment(file)
  341. });
  342. } else {
  343. console.log("Could not parse file name from player JSON.");
  344. }
  345. });
  346. }
  347. },
  348. tracksFromPage: function(data) {
  349. if (data && data.files) {
  350. var self = this;
  351. $.each(data.files, function(file, val){
  352. if (mbz.archive_org_importer.isAudioFile(val.format)) {
  353. var fileName = file.replace(/^\//, ''); // remove leading slash
  354. var duration = MBZ.Util.hmsToSeconds(val.duration);
  355. duration = Math.round(parseFloat(duration) * 1000); // sec to msec
  356. if (isNaN(duration)) {
  357. duration = null;
  358. }
  359.  
  360. self.tracks.updateData({
  361. med: 0,
  362. tit: val.title,
  363. dur: duration,
  364. file: fileName
  365. });
  366. }
  367. });
  368. }
  369. }
  370. };
  371. /**
  372. * Handle track sources and the displaying of those data.
  373. */
  374. mbz.archive_org_importer.Release.Tracks = function() {
  375. /**
  376. * Target element to display track source contents.
  377. */
  378. var contentHtml = $('<div>');
  379. /**
  380. * Store parsed track data objects to allow multiple data editing passes.
  381. */
  382. var tracks = {};
  383. /**
  384. * Track data sources available.
  385. */
  386. var sources = [];
  387. /**
  388. * Track source to use.
  389. */
  390. var selectedSource = null;
  391. /**
  392. * Number of unique valid sources.
  393. */
  394. var validSources = 0;
  395.  
  396. /**
  397. * Add all available track sources to a user dialog.
  398. */
  399. function addSources(show) {
  400. var sourceSelect = $('<select>');
  401.  
  402. sourceSelect.on('change', function(){
  403. selectedSource = this.value;
  404. showSources();
  405. });
  406.  
  407. // add sources
  408. $.each(sources, function(idx, source) {
  409. if (!source.dupe && source.files && source.files.length > 0) {
  410. var sourceTitle = '';
  411. if (source.type == 'player') {
  412. sourceTitle = 'Web Player';
  413. } else {
  414. sourceTitle = 'Playlist (' + source.name + ')';
  415. }
  416. sourceSelect.append('<option value="' + idx + '">'
  417. + sourceTitle + '</option>');
  418. }
  419. });
  420.  
  421. // add elements
  422. this.dEl.append(sourceSelect);
  423. sourceSelect.before('Found multiple track listings with different items.'
  424. + '<br/>Please select a track data source to import: ');
  425. this.dEl.append(contentHtml);
  426. };
  427.  
  428. /**
  429. * Commit currently selected tracks source to be included in MusicBrainz
  430. * submission.
  431. */
  432. this.commit = function(release) {
  433. $.each(sources[selectedSource].files, function(idx, val) {
  434. tracks[val].idx = idx; // reset track number
  435. release.addTrack(tracks[val]);
  436. });
  437. };
  438.  
  439. /**
  440. * Check which track sources are available. Called on page loading.
  441. */
  442. this.detectSources = function() {
  443. // internal player data
  444. var playerJSON = this.getPlayerJSON();
  445. if (playerJSON.length > 0) {
  446. sources.push({
  447. type: 'player',
  448. name: 'web-player',
  449. data: $.parseJSON(playerJSON)
  450. });
  451. }
  452.  
  453. // playlists
  454. $('#ff0 a').each(function(idx, item){
  455. var url = $(item).attr('href');
  456. if (url.endsWith('.m3u')) {
  457. sources.push({
  458. type: 'playlist',
  459. name: MBZ.Util.getLastPathSegment(decodeURIComponent(url)),
  460. url: url
  461. });
  462. }
  463. });
  464.  
  465. if (sources.length > 0) {
  466. // default to first entry
  467. selectedSource = 0;
  468. }
  469. };
  470.  
  471. /**
  472. * Parse track data from all available sources. Called, when import is
  473. * initialized.
  474. * @pageData page data as JSON object
  475. */
  476. this.parseSources = function(pageData) {
  477. var self = this;
  478. var sourceParsedCount = 0;
  479.  
  480. function incParsedCount() {
  481. // increase parsed sources counter
  482. if (++sourceParsedCount == sources.length) {
  483. squashSources.call(self);
  484. if (validSources > 1) {
  485. addSources.call(self);
  486. }
  487. // all data parsed, proceed with import
  488. self.enableImport();
  489. }
  490. }
  491.  
  492. function getTrackList(source) {
  493. if (source.files && source.files.length > 0) {
  494. // looks like data is already set
  495. return;
  496. }
  497. source.files = [];
  498. if (source.type == 'player') {
  499. $.each(source.data, function(idx, val) {
  500. var file = val.sources[0].file;
  501. if (file) {
  502. source.files.push(MBZ.Util.getLastPathSegment(file));
  503. }
  504. });
  505. // done
  506. incParsedCount();
  507. } else if (source.type == 'playlist') {
  508. // needed, since we get redirected to differet subdomain
  509. var url = 'https://cors-anywhere.herokuapp.com/archive.org:443'
  510. + source.url;
  511. $.get(url, function(data) {
  512. //source.data = data;
  513. var files = data.split('\n');
  514. $.each(files, function(idx, file) {
  515. file = MBZ.Util.getLastPathSegment(file.trim());
  516. if (file.length > 0) {
  517. source.files.push(file);
  518. }
  519. });
  520. }, 'text').fail(function(jqxhr, textStatus, error) {
  521. var err = textStatus + ', ' + error;
  522. console.error("Request (" + url + ") failed: " + err);
  523. }).always(function() {
  524. // done
  525. incParsedCount();
  526. });
  527. }
  528. }
  529.  
  530. // First try to parse data from the internal player as a basis. This data
  531. // may be incomplete (cropped track names) so add it first and overwrite it
  532. // later with more complete data from the page's JSON.
  533. $.each(sources, function(idx, val) {
  534. var source = sources[idx];
  535. if (source.type == 'player') {
  536. // parse some track data from the player
  537. self.parseJSON.tracksFromPlayer.call(self, source.data);
  538. }
  539. });
  540.  
  541. // try to get missing data from page's JSON object
  542. if (pageData.files) {
  543. self.parseJSON.tracksFromPage.call(self, pageData);
  544. }
  545.  
  546. // since track data is available, pase the track list for each source
  547. $.each(sources, function(idx, val) {
  548. getTrackList(val);
  549. });
  550. };
  551.  
  552. /**
  553. * Initialize and show the source's track data dialog. Also called, to update
  554. * on track source data select change.
  555. */
  556. this.showSources = function() {
  557. var self = this;
  558. var trackTable = $('<table id="mbzImportTrackTable">'
  559. + '<thead>'
  560. + '<tr>'
  561. + '<td>#</td><td>Title</td><td>Length</td>'
  562. + '</tr></thead></table>');
  563. var trackList = $('<tbody>');
  564.  
  565. $.each(sources[selectedSource].files,
  566. function(idx, val) {
  567. if (tracks[val]) {
  568. var duration = data[val].dur;
  569. duration = (duration ? MBZ.Util.msToHms(duration) : '&mdash;');
  570. trackList.append($('<tr>'
  571. + '<td>' + (idx + 1) + '</td>'
  572. + '<td>' + tracks[val].tit + '</td>'
  573. + '<td>' + duration + '</td>'
  574. + '</tr>'));
  575. } else {
  576. console.warn('No data for file "' + val + '" found.');
  577. }
  578. });
  579.  
  580. trackTable.append(trackList);
  581. contentHtml.html(trackTable);
  582. this.dEl.show();
  583. };
  584.  
  585. /**
  586. * Remove duplicated sources which have the same track lists.
  587. */
  588. function squashSources() {
  589. // go through all source's files
  590. for (var i=0; i<sources.length; i++) {
  591. var src = sources[i];
  592. if (!src.dupe) {
  593. var a = src.files;
  594. if (!a || a.length == 0) {
  595. src.dupe = true;
  596. console.warn("Remove source '" + src.name + "' no files found.");
  597. } else if ((i + 1) < sources.length) {
  598. for (var j=i + 1; j<sources.length; j++) {
  599. var b = sources[j];
  600. if (!b.dupe) {
  601. if (mbz.archive_org_importer.Release.Tracks
  602. .compareSourceFiles(a, b.files)) {
  603. b.dupe = true;
  604. }
  605. }
  606. }
  607. }
  608. }
  609. }
  610.  
  611. // count valid sources
  612. $.each(sources, function(idx, val) {
  613. if (!val.dupe && val.files.length > 0) {
  614. validSources++;
  615. }
  616. });
  617. };
  618.  
  619. /**
  620. * Update track metadata with new values. If a value is already set, it will
  621. * get overwritten with the new one.
  622. */
  623. this.updateData = function(data) {
  624. var isValid = mbz.archive_org_importer.Release.Tracks.isValidTrackData;
  625.  
  626. if (tracks[data.file]) {
  627. var tData = tracks[data.file];
  628. // update
  629. if (isValid(data.med)) {
  630. tData.med = data.med;
  631. }
  632. if (isValid(data.tit)) {
  633. tData.tit = data.tit.trim();
  634. }
  635. if (isValid(data.idx)) {
  636. tData.idx = data.idx;
  637. }
  638. if (isValid(data.dur)) {
  639. tData.dur = data.dur;
  640. }
  641. } else {
  642. // add new
  643. tracks[data.file] = data;
  644. }
  645. };
  646. };
  647. /**
  648. * Check if some track data is valid (i.e. not empty or undefined).
  649. */
  650. mbz.archive_org_importer.Release.Tracks.isValidTrackData = function (dataEntry) {
  651. if (typeof dataEntry !== 'undefined' && dataEntry != null) {
  652. if (typeof dataEntry === 'string') {
  653. if (dataEntry.trim().length > 0) {
  654. return true;
  655. }
  656. return false;
  657. } else {
  658. return true;
  659. }
  660. }
  661. return false;
  662. };
  663. /**
  664. * Compare files for two sources.
  665. */
  666. mbz.archive_org_importer.Release.Tracks.compareSourceFiles = function(a, b) {
  667. if (a.length != b.length) {
  668. return false;
  669. }
  670.  
  671. for (var i=0; i<a.length; i++) {
  672. if (a[i] != b[i]) {
  673. return false;
  674. }
  675. }
  676. return true;
  677. };
  678. mbz.archive_org_importer.Release.Tracks.prototype = {
  679. /**
  680. * Get player JSON data as string.
  681. * @return player JSON data or empty string, if nothing was found
  682. */
  683. getPlayerJSON: function() {
  684. var pJSON = $('#midcol > script').text().trim()
  685. .match(/Play\([\s\S]*?(\[{[\s\S]*}\])/);
  686. if (pJSON && pJSON[1]) {
  687. return pJSON[1];
  688. }
  689. return "";
  690. }
  691. };
  692.  
  693. mbz.archive_org_importer.init = function() {
  694. var pageType = window.location.pathname.split('/');
  695. if (pageType.length >= 2) {
  696. pageType = pageType[1].toLowerCase()
  697. } else {
  698. return;
  699. }
  700.  
  701. if (pageType == 'details' && $('body').hasClass('Audio')) {
  702. // import a release
  703. MBZ.Html.globStyle.append(
  704. '#mbzImportTrackTable {margin-top:0.5em;margin-left:0.5em;}'
  705. + '#mbzImportTrackTable thead {'
  706. + 'font-weight:bold;'
  707. + 'background-color:rgba(115,108,174,0.5);'
  708. + '}'
  709. + '#mbzImportTrackTable tbody td:nth-child(1) {'
  710. + 'border-right:1px solid #666;'
  711. + 'padding-right:0.15em;'
  712. + '}'
  713. + '#mbzImportTrackTable tbody tr:nth-child(odd) {'
  714. + 'background-color:rgba(0,0,0,0.1);'
  715. + '}'
  716. + '#mbzImportTrackTable tbody td:nth-child(2) {'
  717. + 'padding-left:0.3em;'
  718. + '}'
  719. + '#mbzImportTrackTable tbody td:nth-child(3) {'
  720. + 'padding-left:0.3em;'
  721. + 'font-family:courier,monospace;'
  722. + 'text-align:right;'
  723. + '}'
  724. );
  725. //mbz.archive_org_importer.release.init();
  726. new mbz.archive_org_importer.Release();
  727. } else if (pageType == 'bookmarks.php') {
  728. // check all bookmarks for MusicBrainz relations
  729. var links = $('.box>table>tbody a').filter(function(idx) {
  730. // no way to check type for audio here
  731. return $(this).attr('href').startsWith('/details/');
  732. });
  733. var control = $('<div id="mbzControls">');
  734. $('.box>h1').after(control);
  735. if (links.length > 0) {
  736. mbz.archive_org_importer.linkCheck.scan({
  737. links: links,
  738. controlEl: control
  739. });
  740. }
  741. } else if (pageType == 'search.php') {
  742. var links = [];
  743. // check audio links for MusicBrainz relations
  744. var audioItems = $('.numberCell>img[alt="[audio]"]').filter(function(idx) {
  745. // get the first linked audio item..
  746. var el = $(this).parent().next().children('a')[0];
  747. if (el) {
  748. el = $(el);
  749. if (el.attr('href').startsWith('/details/')) {
  750. // ..and extract it's url
  751. links.push(el);
  752. }
  753. }
  754. });
  755. var control = $('<div>');
  756. var col = $('<td colspan="2">');
  757. col.append(control);
  758. var row = $('<tr>').append(col);
  759. $('.resultsTable').prepend(row);
  760. if (links.length > 0) {
  761. mbz.archive_org_importer.linkCheck.scan({
  762. links: links,
  763. controlEl: control
  764. });
  765. }
  766. }
  767. };
  768.  
  769. mbz.archive_org_importer.init();