Hatena::Blog::Code404

日々のことをつらつらと。

MuxtapeMp3Downloader.uc.js

ユーザーページのプレイリストにダウンロード用のリンクを提供する。ダウンロードされるファイル名は、そのプレイリスト上の Artist - Title.mp3 になる。リンクをクリックしても何も起きてないように見えるが、ちゃんとダウンロードされている。どうも、 Muxtape 上のプレイヤーで再生・読み込みをしても(ブラウザ側に?)キャッシュされないのか、一からダウンロードする。

一部、 YoutubeFlvDownloaderNicovideoDownloader のコードを拝借したため、こっちで公開。あっちは全部自分のにしたいから。

著作権物の違法ダウンロードには注意してね。知らないよ。

// ==UserScript==
// @name       Muxtape MP3 Downloader
// @version    0.0.1
// @description  Adds MP3 download links on Muxtape.
// @author     Shinya
// @homepage     
// @namespace    http://www.code-404.net/
// @compatibility  Firefox 2.0
// @include    chrome://browser/content/browser.xul
// @note       
// ==/UserScript==


(function(){
  var MuxtapeDownloader = {
    init: function(aEvent){
      var doc = aEvent.originalTarget; // document object
      
      // Don't work without muxtape.com
      if(doc.location.href.match(/http:\/\/\w+\.muxtape\.com\/\w*/) == null) return;
      
      // Gets file URIs
      var uris = {};
      var win = doc.window;
      for(var obj in win){
        if(win[obj].ladles != undefined){
          for(var song in win[obj].ladles){
            uris["song" + win[obj].ladles[song].hex] = win[obj].ladles[song].songurl;
          }
          break;
        }
      }
      
      
      // Adds download links
      var songs = $X(".//li[@class='song']", doc.body);
      for(var i = 0, l = songs.length; i < l; i++){
        var span = document.createElementNS("http://www.w3.org/1999/xhtml", "span");
        $X(".//div[@class='info']", songs[i])[0].appendChild(span);
        
        span.setAttribute("class", "buy");
        
        var anchor = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
        span.appendChild(anchor);
        span.insertBefore(document.createTextNode(" - "), span.lastChild);
        
        anchor.setAttribute("href", uris[songs[i].id]);
        anchor.addEventListener("click", MuxtapeDownloader.download, false); //
        anchor.appendChild(document.createTextNode("Download this MP3"));
      }
    },
    
    download: function(aEvent){
      var downloader = new MuxtapeDownloader.downloader();
      downloader.download(aEvent.target);
      aEvent.preventDefault();
      aEvent.stopPropagation(); // Don't action muxtape player
    }
  };
  
  // Like a "window.addEventListener('load', MuxtapeDownloader.init, false);
  document.getElementById("appcontent").addEventListener("DOMContentLoaded", MuxtapeDownloader.init, false);
  
  
  MuxtapeDownloader.downloader = function(){
    this.SKIP_SELECT_PROMPT = true;
  }
  
  MuxtapeDownloader.downloader.prototype = {
    download: function(aNode){
      // Get "Artist - Title" (File name)
      var title = $X("../../../div[@class='name']/text()", aNode, String);
      title = title.replace(/^\s+/, "").replace(/\s+$/, "").replace(/[\$\*\?\!\:\;\,\<\>\|\"\'\/\\]/g, "_");
      
      this._title = title;
      this._uri = aNode.getAttribute("href");
      this._ioService = Components.classes["@mozilla.org/network/io-service;1"]
        .getService(Components.interfaces.nsIIOService);
      
      /* Do without comment if you want download after resource checked
      this.wrappedJSObject = this;
      var uri = this._ioService.newURI(aNode.getAttribute("href"), null, null);
      var httpChannel = this._ioService.newChannelFromURI(uri)
        .QueryInterface(Components.interfaces.nsIHttpChannel);
      httpChannel.requestMethod = "GET";
      httpChannel.redirectionLimit = 0;
      httpChannel.asyncOpen(this.getListener(), this);
      */
      
      // Do comment out if you want download after resource checked
      this.saveMp3(this._title, this._uri);
      
    },
    
    /* Do without comment if you want download after resource checked
    getListener: function(){
      var listener = {
        onStartRequest: function(){},
        onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount){
          aContext.wrappedJSObject.onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount);
        },
        onStopRequest: function(){}
      }
      return listener;
    },
    
    onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount){
      aRequest.QueryInterface(Components.interfaces.nsIHttpChannel);
      alert(aRequest.responseStatus); //
      if(aRequest.responseStatus != 200){
        alert("ERROR: Access Denied!! Try again later...");
        return;
      }
      this.saveMp3(this._title, this._uri);
    },
    */
    
    saveMp3: function(aTitle, aMp3UriSpec){
      const EXTENSION = "mp3";
      var CONTENT_TYPE = "audio/mpeg";
      
      var fileName = aTitle + "." + EXTENSION;
      var fileUri = this._ioService.newURI(aMp3UriSpec, null, null)
        .QueryInterface(Components.interfaces.nsIURL);
      var fileInfo = new FileInfo(fileName, fileName, aTitle, EXTENSION, fileUri);
      
      var fpParams = {
        fpTitleKey: "",
        isDocument: false,
        fileInfo: fileInfo,
        contentType: CONTENT_TYPE,
        saveMode: GetSaveModeForContentType(CONTENT_TYPE),
        saveAsType: 0,
        file: null,
        fileURL: null
      };
      if(!getTargetFile(fpParams, this.SKIP_SELECT_PROMPT)){
        return;
      }
      
      var fileURL = fpParams.fileURL || makeFileURI(fpParams.file);
      
      var persist = makeWebBrowserPersist();
      var nsIWebBrowserPersist = Components.interfaces.nsIWebBrowserPersist;
      persist.persistFlags = 
        nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES ||
        nsIWebBrowserPersist.PERSIST_FLAGS_FROM_CACHE;
      
      var transfer = Components.classes["@mozilla.org/transfer;1"]
        .createInstance(Components.interfaces.nsITransfer);
      transfer.init(fileInfo.uri, fileURL, "", null, null, null, persist);
      persist.progressListener = transfer;
      persist.saveURI(fileInfo.uri, null, null, null, null, fileURL);
    }
    
  }
  
  
  
  //=====================================
  // XPath function from nulog
  // http://lowreal.net/blog/2007/11/17/1
  // 
  // Thanks, cho45.
  //=====================================
  
  function $X (exp, context, type /* want type */) {
    if (typeof context == "function") {
      type  = context;
      context = null;
    }
    if (!context) context = document;
    var exp = (context.ownerDocument || context).createExpression(exp, function (prefix) {
      var o = document.createNSResolver(context).lookupNamespaceURI(prefix);
      if (o) return o;
      return (document.contentType == "application/xhtml+xml") ? "http://www.w3.org/1999/xhtml" : "";
    });
  
    switch (type) {
      case String:
        return exp.evaluate(
          context,
          XPathResult.STRING_TYPE,
          null
        ).stringValue;
      case Number:
        return exp.evaluate(
          context,
          XPathResult.NUMBER_TYPE,
          null
        ).numberValue;
      case Boolean:
        return exp.evaluate(
          context,
          XPathResult.BOOLEAN_TYPE,
          null
        ).booleanValue;
      case Array:
        var result = exp.evaluate(
          context,
          XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
          null
        );
        var ret = [];
        for (var i = 0, len = result.snapshotLength; i < len; i++) {
          ret.push(result.snapshotItem(i));
        }
        return ret;
      case undefined:
        var result = exp.evaluate(context, XPathResult.ANY_TYPE, null);
        switch (result.resultType) {
          case XPathResult.STRING_TYPE : return result.stringValue;
          case XPathResult.NUMBER_TYPE : return result.numberValue;
          case XPathResult.BOOLEAN_TYPE: return result.booleanValue;
          case XPathResult.UNORDERED_NODE_ITERATOR_TYPE: {
            // not ensure the order.
            var ret = [];
            var i = null;
            while (i = result.iterateNext()) {
              ret.push(i);
            }
            return ret;
          }
        }
        return null;
      default:
        throw(TypeError("$X: specified type is not valid type."));
    }
  }
})();