Subsonicが遅すぎるので検索+ストリーミングを自作した。

subsonicが自分のnasですと遅すぎます。
512M/1.4GCeleronMでは10万ファイルをハンドルするのはしんどい。表示に10秒、検索に10秒とか所要する。
検索も直感的ではない。もう少しシンプルで凶暴なもので良いかな、と。
ruby+sinatra+mongoid+taglib2で行きました。

制作物

http://modeverv.dyndns.org/mediadb
外側からのテストはしていないので配信速度以前に上手く動くか
よくわからない。

機能

ファイルシステムを走査してmongodbに突っ込む
mongodbを絞り込み検索して結果をストリーミング(笑)する

結果

limit(1000)にしている検索が数秒でで返ってくるので実用的と思う。
正規表現に日本語突っ込むと*5ぐらいの秒数を所要する。実用の範囲内か。limit(100)だと500ms以下になる。
思いの外短いコードで実現できた。特にストリーミング(笑)部分。
ストリーミングをsubsonicのようにエンコード仕掛けると良いかも知れないがcpuが追いつかない。

mongodbをubuntu11で

mongodbはスキーマレスなので作り易い。
mongodbでもindexを使うとselectがかなり速く、低負荷になる。これはちょっとした発見だった。
http://www.mongodb.org/display/DOCS/Ubuntu+and+Debian+packages
を見ながらインストール。

taglib

taglibはmp3からm4aからいろいろ同じコードでタグが取得できる。そしてとても速い。
apt-get install taglibのdev系とか
gem install ruby-taglib2

ファイルシステムを走査してmongodbに突っ込む部分

#mediamodel.rb
#! /usr/bin/env ruby
require 'rubygems'
require 'mongoid'
require 'taglib2'
require 'kconv'

Mongoid.configure do |config|
 config.master = Mongo::Connection.new.db('media-mongoid')
end

class Mediamodel
  include Mongoid::Document
  field :genre, type: String, :default => ''
  field :artist,type: String, :default => ''
  field :album, type: String, :default => ''
  field :title, type: String, :default => ''
  field :year,  type: String, :default => ''
  field :type,  type: String, :default => ''
  field :search,type: String, :default => ''
  field :path,  type: String, :default => ''
  field :created_at, :type => DateTime, :default => Time.now
  index :search
  
  def set_search
    self.search = "#{genre}|#{artist}|#{album}|#{title}|#{year}|#{path}"
  end
  
  class << self
    def path2object(path)
      a_media = Mediamodel.new(
                               :codec => File.extname(path),
                               :type  => File.extname(path),
                               :path  => path)
      begin  
        tag = TagLib2::File.new(path)
        a_media.genre  = tag.genre.to_s.toutf8  if tag.genre
        a_media.artist = tag.artist.to_s.toutf8 if tag.artist
        a_media.album  = tag.album.to_s.toutf8  if tag.album
        a_media.title  = tag.title.to_s.toutf8  if tag.title
        a_media.year   = tag.year.to_s.toutf8   if tag.year
      rescue =>ex
#        p ex
      end
      a_media.set_search
      return a_media
    end
  end
end
# glob_server.rb
#! /usr/bin/env ruby

require File.dirname(__FILE__)+'/mediamodel'

class GlobServer
  attr_accessor :files
  
  def initialize(args = {})
    @server = args[:server] ||= "/var/smb/sdb1"
    @folders = args[:folders] ||= [
                                   'music/iTunes1',
                                   'music/iTunes2',
                                   'music/iTunes3',
                                   'music/iTunes2011',
                                   'music/iTunesMac',
                                   'music/iTunesLossless',
                                   'video',
                                   'video2',
                                  ] 
    @ext = args[:ext] ||= ['mp3','mp4','m4a','wav','flv','mka','ape','flac','wmv','mka','mkv']
    @files = []
  end

  def media_glob
    @folders.each do |p|
      @ext.each do |e|
        Dir.glob("#{@server}/#{p}/**/*.#{e}") do |element|
          @files << {:path => element}
        end
      end
    end
  end
end

gs = GlobServer.new
gs.media_glob

gs.files.each do |e|
  p e[:path]
  unless Mediamodel.where(:path => e[:path]).first.nil?
    p "find in db"
    next
  end
  model = Mediamodel.path2object(e[:path])
#  p model
  model.save
end

sinatraのweb

# main.rb
#! /home/seijiro/.rvm/rubies/ruby-1.9.2-p290/bin/ruby
# -*- coding:utf-8 -*-

require 'sinatra'
require "sinatra/reloader" if development?
require File.dirname(__FILE__)+'/mediamodel'

# need modify! ################
def make_path(path)
  path
end
###############################

get '/' do
  erb :index
end

get '/api/search' do
  qs = params['qs'].split(' ').map {|q| /#{q}/i}
  ret = Mediamodel.where(:search.all => qs).limit(100); #TODO injection??
  ret.to_json
end

get '/api/m3u/:mediaid' do
  content_type 'audio/x-mpegurl'
  "http://#{request.host}/mediadb/api/stream/#{params[:mediaid]}"
end

get '/api/stream/:mediaid' do
  ret = Mediamodel.find(params[:mediaid])
  if ret
#no need for vlc@mac    content_type 'audio/mpeg' 
    File.open(make_path(ret.path),'rb').read
  else
    "error"
  end
end
# public/mediadb.js
var _server = {
    search : function(qstring) {
        this._server_get('search',qstring);
    },
    _server_get : function(uri,pdata){
        $.ajax({  
                   type: "GET",
                   url: this._prefix + "/" + uri ,
                   data: "qs="+pdata,
                   success: function(msg){
                       console.log( "Data Saved: " + msg );
                       _page.emit(eval(msg));
                   }
               });    
    },
    _prefix : "/mediadb/api"
};
var _page = {
    emit : function(json){
        $("#echoarea").html(":"+json.length);
        $("#wrapper").html('');
        for(var i =0;i< json.length;i++){
            var elem = "<a href='/mediadb/api/m3u/"+ json[i]['_id'] + "'><li>";
            if(json[i]['genre']) { elem += json[i]['genre'] + '/';}
            if(json[i]['artist']){ elem += json[i]['artist']+ '/';}
            if(json[i]['year'])  { elem += json[i]['year']  + '/';}
            if(json[i]['album']) { elem += json[i]['album'] + '/';}
            if(json[i]['title']) { elem += json[i]['title'] + '/';}
            if(json[i]['type'])  { elem += json[i]['type'].replace('.','')  + '/';}
            if(json[i]['path'])  { elem += json[i]['path'].replace(/(^.*\/)(.*)$/,"$2");}
            elem += "</li></a>";
            var element = $(elem);
            element.mouseover(
                function(){
                    $(this).css({'font-size':'2em'});
                });
            element.mouseout(
                function(){
                    $(this).css({'font-size':'1em'});
                });
            $("#wrapper").append(element);
        }
        $('#grayout').toggle();
    }
};
function run(){
  var qstring = $('#query').val();
  $('#grayout').toggle();
  console.log(qstring);
  _server.search(qstring);
  return false;
}
# view/index.erb
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>mediadb</title>
    <link rel="stylesheet" href="/mediadb/pc.css" type="text/css" media="screen" />
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>

    <script type="text/javascript" src="/mediadb/mediadb.js?<%=Time.now.to_i.to_s%>"></script>

  </head>
  <body>
    <header>
      <h1>mediadb<span id="echoarea"></span></h1>
      <div class="search">
        <form onsubmit="run();return false;" style="float:left;">
          <input type="text" style="float:left;" class="text_field" id="query">
          <input type="button" style="float:left;" class='submit_button' onclick="run();return false;" value="Search">
        </form>
      </div>
    </header>
    <div id="grayout">&nbsp;</div>
    
    <div id="wrapper">&nbsp;</div>

    <footer><p>modeverv@gmai.com</p></footer>
  </body>
</html>

m3uダウンロードをjsで完結させる

凶暴になりました。ページのほうが情報を持っているのにもかかわらずidをキーにサーバーサイドでもう一度データを取得するという無駄な処理がキャンセルされました。
サーバーとは検索の時とストリーミングの時に通信することになります。

# m3uをjsで生成して吐き出させるようにした。
# 変更部のみ
var _page = {
    emit : function(json){
        this._json = json;
        $("#echoarea").html(":"+json.length);
        $("#wrapper").html('');
        for(var i =0;i< json.length;i++){
            var elem = "<a><li onclick='m3u(\"" ;
            elem += json[i]['_id'] + "\",\"" ;
            elem += json[i]['path'].replace(/(^.*\/)(.*)$/,"$2") + "\");return false;'>";
            if(json[i]['genre']) { elem += json[i]['genre'] + '/';}
            if(json[i]['artist']){ elem += json[i]['artist']+ '/';}
            if(json[i]['year'])  { elem += json[i]['year']  + '/';}
            if(json[i]['album']) { elem += json[i]['album'] + '/';}
            if(json[i]['title']) { elem += json[i]['title'] + '/';}
            if(json[i]['type'])  { elem += json[i]['type'].replace('.','')  + '/';}
            if(json[i]['path'])  { elem += json[i]['path'].replace(/(^.*\/)(.*)$/,"$2");}
            elem += "</li></a>";
            var element = $(elem);
            $("#wrapper").append(element);
        }
        $('#grayout').toggle();
    },
    _json : ""
};

function m3u(id,title){
    var elem = "#EXTM3U\n";
    elem += "#EXTINF:100, - " + title + "\n";
    elem += "http://" + location.host + _server._prefix + "/stream/" + id + "\n" ;
    console.log(elem);
    location.href = 'data:audio/x-mpegurl,'+encodeURIComponent(elem);
}

function download(){
    var json = _page._json;
    var elem = "#EXTM3U\n";
    for(var i =0;i< json.length;i++){
        elem += "#EXTINF:100,";
        elem += json[i]['artist'] ?  json[i]['artist']+ " - " : " - ";
        elem += json[i]['title']  ?  json[i]['title'] + "\n"  : json[i]['path']  + "\n";
        elem += "http://" + location.host + _server._prefix + "/stream/" + json[i]['_id'] + "\n" ;
    }
    location.href = 'data:audio/x-mpegurl,'+encodeURIComponent(elem);
}
#serverサイドは
=begin
get '/api/m3u/:mediaid' do
  content_type 'audio/x-mpegurl'
  "http://#{request.host}/mediadb/api/stream/#{params[:mediaid]}"
end
=end
#ボタン追加
<input type="button" style="float:left;" class='submit_button' onclick="download();return false;" value="M3U">