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"> </div> <div id="wrapper"> </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">