mpd(Music Player Daemon)の互換データベースをRubyで出力する

mpd(Music Player Daemon)はLinux/UNIXでmp3/ogg/flac等の音楽を再生するデーモンです。音楽プレイヤとしては珍しくインターフェースになるクライアントと、音楽再生を行うデーモンから成るクライアント/サーバの構成を取っており、GTKを使うような通常のGUIクライアント(sonataなど)の他にコマンドライン(mpc/ncmcpなど)やWeb用(phpMpなど)の様々なクライアントが開発されています。

このように大変魅力的(?)なソフトウェアなのですが、日本語を含むid3tagはバージョンを問わずshift-jisでエンコーディングされていることが多く、このようなケースではmpd --update-dbして作成したデータベースは0xc2や0xc3が混ざった不可解な変換がされ文字化けしてしまいます。(※id3v1はmpd.confのid3v1_encodingで指定可能ですが、これだけだと実用上あまりうれしくありません)

mpdに自力でパッチを作ろうかとも思ったのですが、ソースを読んでみたら想像より面倒くさそうだったので、使い慣れたRubyでmpd互換のtag_cacheを吐き出すスクリプトを書くことにしました。流行っていないようですがgemにあったruby-audioinfoというライブラリがmp3の他にoggflacもまとめて扱えるようだったので、これを使いました。

…と、万事これでうまくいくかと思いきや、ruby-audioinfo内部で呼んでいるruby-mp3infoもid3tagの文字コード関係は鬼門で、調べてみるとid3v2は実質的にutf-8しか扱えないようです。結局ファイルからタグを読んだ時点で強引にkconvutf-8に変換するよういくつかのメソッドをコピペして再定義せざるを得ませんでした。

かなり酷いスクリプトではありますが、どうにかこれでmpd互換のデータベースが文字化けせずに出力できています。もっと良い方法がありそうなのですが…うーん(*_*)。

#mpd互換のtag_cacheデータベースを標準出力するスクリプト
require 'rubygems'
require 'audioinfo' #gem install ruby-audioinfo --remote
require 'kconv'

#TODO:mpd.confのmusic_directoryを指定
MUSIC_DIR = ""
raise "set MUSIC_DIR !" if MUSIC_DIR == ""

#ここから機能拡張と文字化け防止のため再定義
#それぞれ以下のバージョンが前提、コピペなのでこれ以外だと動作しない
#ruby-audioinfo-0.1.4
#ruby-mp3info-0.6.10

#AudioInfoの再定義
#ジャンルを取得できるようにするため
class AudioInfo
    attr_reader :genre #ジャンルを追加

  def initialize(fn, encoding = 'utf-8')
    raise(AudioInfoError, "path is nil") if fn.nil?
    @path = fn
    ext = File.extname(@path)
    raise(AudioInfoError, "cannot find extension") if ext.empty?
    @extension = ext[1..-1].downcase
    @musicbrainz_infos = {}
    @encoding = encoding

    begin
      case @extension
	when 'mp3'
	  @info = Mp3Info.new(fn, :encoding => @encoding)
	  default_tag_fill
	  if (arr = @info.tag2["TXXX"]).is_a?(Array)
	    fields = MUSICBRAINZ_FIELDS.invert
	    arr.each do |val|
	      if val =~ /^MusicBrainz (.+)\000(.*)$/
		short_name = fields[$1]
	        @musicbrainz_infos[short_name] = $2
	      end
	    end
	  end
          @bitrate = @info.bitrate
	  i = @info.tag.tracknum
	  @tracknum = (i.is_a?(Array) ? i.last : i).to_i
	  @length = @info.length.to_i
	  @date = @info.tag["date"]
	  @vbr = @info.vbr
	  @genre = @info.tag["genre_s"] #ジャンルを追加
	  @info.close

	when 'ogg'
	  @info = OggInfo.new(fn, @encoding)
	  default_fill_musicbrainz_fields
	  default_tag_fill
          @bitrate = @info.bitrate/1000
          @tracknum = @info.tag.tracknumber.to_i
	  @length = @info.length.to_i
	  @date = @info.tag["date"]
	  @vbr = true
	  @info.close
	  
	when 'mpc'
	  fill_ape_tag(fn)
	  
	  mpc_info = MpcInfo.new(fn)
          @bitrate = mpc_info.infos['bitrate']/1000
	  @length = mpc_info.infos['length']

        when 'ape'
	  fill_ape_tag(fn)

        when 'wma'
	  @info = WmaInfo.new(fn, :encoding => @encoding)
	  @artist = @info.tags["Author"]
	  @album = @info.tags["AlbumTitle"]
	  @title = @info.tags["Title"]
	  @tracknum = @info.tags["TrackNumber"].to_i
	  @date = @info.tags["Year"]
	  @bitrate = @info.info["bitrate"]
	  @length = @info.info["playtime_seconds"]
	  MUSICBRAINZ_FIELDS.each do |key, original_key|
	    @musicbrainz_infos[key] = 
              @info.info["MusicBrainz/" + original_key.tr(" ", "")] ||
              @info.info["MusicBrainz/" + original_key]
	  end
          
	when 'aac', 'mp4', 'm4a'
	  @info = MP4Info.open(fn)
	  @artist = @info.ART
	  @album = @info.ALB
	  @title = @info.NAM
	  @tracknum = ( t = @info.TRKN ) ? t.first : 0
	  @date = @info.DAY
	  @bitrate = @info.BITRATE
	  @length = @info.SECS
	  mapping = MUSICBRAINZ_FIELDS.invert

	  `faad -i #{fn.shell_escape} 2>&1 `.grep(/^MusicBrainz (.+)$/) do
	    name, value = $1.split(/: /, 2)
	    key = mapping[name]
	    @musicbrainz_infos[key] = value
	  end
	
	when 'flac'
	  @info = FlacInfo.new(fn)
          tags = convert_tags_encoding(@info.tags, "UTF-8")
	  @artist = tags["ARTIST"]
	  @album = tags["ALBUM"]
	  @title = tags["TITLE"]
	  @tracknum = tags["TRACKNUMBER"].to_i
	  @date = tags["DATE"]
	  @length = @info.streaminfo["total_samples"] / @info.streaminfo["samplerate"].to_f
	  @bitrate = File.size(fn).to_f*8/@length/1024

	else
	  raise(AudioInfoError, "unsupported extension '.#{@extension}'")
      end

      if @tracknum == 0
        @tracknum = nil
      end

      @musicbrainz_infos.delete_if { |k, v| v.nil? }
      @hash = { "artist" => @artist,
	"album"  => @album,
	"title"  => @title,
	"tracknum" => @tracknum,
	"date" => @date,
	"length" => @length,
	"bitrate" => @bitrate,
      }

    rescue Exception, Mp3InfoError, OggInfoError, ApeTagError => e
      raise AudioInfoError, e.to_s, e.backtrace
    end

    @needs_commit = false

  end
end

#MP3Info関連の再定義
#文字化け防止のため読んだ端からUTF8化する
class Mp3Info
private
  def gettag1
    @tag1_parsed = true
    #@tag1["title"] = @file.read(30).unpack("A*").first
    @tag1["title"] = @file.read(30).toutf8 #読み込み時にUTF8化
    #@tag1["artist"] = @file.read(30).unpack("A*").first
    @tag1["artist"] = @file.read(30).toutf8 #読み込み時にUTF8化
    #@tag1["album"] = @file.read(30).unpack("A*").first
    @tag1["album"] = @file.read(30).toutf8 #読み込み時にUTF8化
    year_t = @file.read(4).to_i
    @tag1["year"] = year_t unless year_t == 0
    comments = @file.read(30)
    if comments.getbyte(-2) == 0
      @tag1["tracknum"] = comments.getbyte(-1).to_i
      comments.chop! #remove the last char
    end
    @tag1["comments"] = comments.unpack("A*").first
    @tag1["genre"] = @file.getbyte
    @tag1["genre_s"] = GENRES[@tag1["genre"]] || ""

    # clear empty tags
    @tag1.delete_if { |k, v| v.respond_to?(:empty?) && v.empty? }
    @tag1.delete("genre") if @tag1["genre"] == 255
    @tag1.delete("tracknum") if @tag1["tracknum"] == 0
  end
end

#ID3v2の場合の処理
class ID3v2
  ### Read a tag from file and perform UNICODE translation if needed
  def decode_tag(name, raw_value)
    puts("decode_tag(#{name.inspect}, #{raw_value.inspect})") if $DEBUG
    case name
      when "COMM"
        #FIXME improve this
	encoding, lang, str = raw_value.unpack("ca3a*") 
	out = raw_value.split(0.chr).last
      when /^T/
	encoding = raw_value.getbyte(0) # language encoding (see TEXT_ENCODINGS constant)   
	out = raw_value[1..-1] 
	# we need to convert the string in order to match
	# the requested encoding
	if out && encoding != @text_encoding_index
	  begin
	    #Iconv.iconv(@options[:encoding], TEXT_ENCODINGS[encoding], out).first
	    out.toutf8 #決めうちでUTF8化
	  rescue Iconv::Failure
	    out
	  end
	else
	  out
	end
      else
        raw_value
    end
  end
end
#再定義ここまで

#music_directory内でのパスを整形
def format_relpath(path)
    if path[0,1] != "/"
	return path
    else
	return path[1..-1]
    end
end

#songListのエントリを出力する
def print_song_list_entry(path, path_in_music = "")
    info_entry = ""
    info_entry << "key: " + path + "\n"
    file = format_relpath(path_in_music + '/' + path)
    info_entry << "file: " + file + "\n"

    begin
	AudioInfo.open(path){|info|
    	    info_entry << "Time: " + info.length.to_s + "\n"
    	    info_entry << "Artist: " + info.artist.to_s + "\n" if info.artist
    	    info_entry << "Title: " + info.title.to_s + "\n" if info.title
    	    info_entry << "Album: " + info.album.to_s + "\n" if info.album
    	    info_entry << "Track: " + info.tracknum.to_s + "\n" if info.tracknum
    	    info_entry << "Genre: " + info.genre.to_s + "\n" if info.genre
	}
    	info_entry << "mtime: " + File.mtime(path).to_i.to_s + "\n"
    rescue AudioInfoError
	#非対応のファイル形式も例外で捕捉する
	STDERR.puts(file)
	STDERR.puts($!)
	return
    end

    print info_entry
end

#ディレクトリを辿ってリストを出力
def search_directory(path, path_in_music = "")
    old_dir = Dir.pwd

    dir = Dir.open(path)
    Dir.chdir(path)
    file_list = []
    dir_list = []

    #ディレクトリとファイルの一覧を作成
    dir.each{|item|
	next if item.match(/^\./) #ドットファイルは読み飛ばす

	if FileTest.directory?(item)
	    dir_list << item
	else
	    file_list << item
	end	
    }

    #ディレクトリをソートして再帰的に辿る
    dir_list.sort.each{|dir|
	    puts "directory: " + dir
	    relpath = format_relpath(path_in_music + '/' + dir)

	    puts "begin: " + relpath 
	    search_directory(dir, relpath)
	    puts "end: " + relpath
    }

    #ファイルをsongListにして出力する
    puts "songList begin"
    file_list.sort.each{|file|
	print_song_list_entry(file, path_in_music)
    }
    puts "songList end"

    Dir.chdir(old_dir)
end

#info出力(定型)
info =<<INFO
info_begin
mpd_version: 0.13.2
fs_charset: UTF-8
info_end
INFO
print info

#リスト出力
search_directory(MUSIC_DIR)

vimでJavaを書く

JavaだとEclipseがある所為かvimで開発するための情報をそれほど見かけません。最低限使えるようにするにはZDNetの以下の記事がありますが、IDEのサポートに慣れているとctagsでは力不足を感じてしまいます。

Eclipse並みとはいわなくても、特に補完はもう少しがんばって欲しいところです。ざっと調べたところvimJava向けの強力な補完を利用するには、今のところjavacomplete、vjde、eclimの3つの方法があるようなので試してみました。

javacomplete

javacompleteはVim7で導入されたオムニ補完(omni-completion)をJavaに適用するものです。個人的にはシンプルで好きですが、ダウンロード数は2000程度なのであまり流行っていないようです。

インストール
unzip javacomplete.zip -d /usr/share/vim/vimfiles

~/.vimrcなどに以下を追加。

autocmd FileType java :setlocal omnifunc=javacomplete#Complete
autocmd FileType java :setlocal completefunc=javacomplete#CompleteParamsInfo
使い方

オムニ補完はデフォルトでです。.をタイプした後や、クラスやメソッドのタイプ中に補完が効きます。import文でももちろん使えます。

vjde

Just a Development Environment for VIM。名前のとおり補完のほか定義の検索やコンパイルなど開発環境的な機能があります。試していませんがRubyを使うとC++の開発もできるようです。

インストール
tar -zxvf vjde.tgz /usr/share/vim/vimfiles
chmod +x /usr/share/vim/vimfiles/plugin/vjde/readtags
使い方

補完はから。

クラスやメソッドなどの上で:Vjdeiすると定義を検索できます。

:Vjdei

vimから編集中のファイルをコンパイルすることもできます。

:Vjdec

ヘルプは以下で見られます。

:helptags /usr/share/vimi/vimfiles/doc
:h vjde

eclim

eclimはEclipseの機能をvimから使うためのEclipseプラグインvim scriptのセットです。付属のeclimdというデーモン経由で動きます。

Eclipseの機能の一部がそのまま使えるということで大変期待していたのですが、vimが大量のエラーを吐いたり、パスまわりで色々と文句を言われたりして、僕の環境ではうまく動かせませんでした。現在のバージョンはJDK1.5を要求するようなので、そのあたりで引っかかったのかなあ…。

こちらのエントリで知りました。インストール方法も読みやすくまとまっており参考になりました。

vimでjavaの開発を行う その1 - Doge log

壊れたかもしれないハードディスクからのデータサルベージ

2013年12月9日追記:

少し冗長なので、新ブログでリライトしました。あわせてこちらもご覧ください。

http://www.xmisao.com/2013/12/02/hdd-salvage-by-dd.html

発端

今月上旬、Webブラウジング中にハードディスクが「カターン」と断末魔をあげてPCがフリーズ、それ以来そのマシンでOSが立ち上がらなくなるというトラブルに遭遇しました。

もちろんディスクトラブルを疑いましたが、結論からいうと実はマザーボードが壊れていてハードディスクが巻き添えを食った形でした。幸いハードディスクはWindowsが入っていたパーティションの一部が論理的に破壊されただけで無事、必死のサルベージ作業の甲斐もありほとんどのデータが復旧できました。

以下はLinuxを使って挙動の妖しいハードディスクからデータをサルベージする方法の備忘録です。当初はディスクが物理的に壊れているかもしれないと思って作業していましたから、もう少し重傷な場合でもそのまま適応できると思います。

方針

壊れているかもしれないディスクを不用意に動かしたくないため、とりあえずディスクイメージを作成し、あとからごにょごにょすることにしました。トラブルに見舞われたマシンそのものが信用ならないので、まず問題のディスクを取り外し正常に動くとわかっているLinuxマシン(Debian etch)に接続。そのマシンにはサルベージ用に少なくとも壊れたハードディスクより大きな容量を持つハードディスクを準備しておきます。

ディスクイメージの作成

恐る恐るマシンを起動してdmesgを眺めると壊れているかもしれないハードディスクはどうにか/dev/hdbとして認識されているようでした。

ディスクイメージはシンプルにddで作成します(※)。以下の例はディスクをMBRも含めて全て読み込んでファイルとして出力する例です。

dd if=/dev/hdb of=/root/hdd.img bs=512 obs=1024k count=488397168 conv=sync,noerror

ifは入力でofは出力です。今回は入力を/dev/hdbに、出力を/root/hdd.imgというファイルしました。

bsは読み込み単位でセクタサイズの512が最小。bsは大きくすると高速化しますが、読み込めない場合は読み込み単位すべてがダメになるのでセクタサイズ(512byte)と同一に指定するのが良いらしいです。

逆にobsは出力する単位で、こちらはバッファリングするだけですので大きな値にすると高速化できるそうです。

countは後述するnoerrorで終端が無効になって無限ループするのを防ぐために指定します。countはbs単位です。countの値はdmesgやfdisk -luなどの出力などを見てハードディスク全体の長さになるよう決めます。

convはオプションでsyncは読み込めないセクタがあってもヌルパディングしてデータの位置を保持する、noerrorはエラーがあっても無視して続けることをそれぞれ表しています。

このコマンドを実行すると途中のエラーなどの情報が標準出力(エラー出力?)に出るのでお決まりで>log 2&>1などとして標準出力/エラー出力をリダイレクトすると良いかも知れません。

ddに与えるオプション指定については次のサイトを参考にさせていただきました。

またそもそもサルベージ対象のハードディスクを認識しない場合もatacontrolを使うと認識される場合もあるようです。

(※)僕はやりませんでしたが、不安定なディスクからデータを読む場合はDMAではなくPIOを使った方が良いという情報もあります。

ディスクイメージのマウントとデータ吸い出し

ちょっと困ったのはこのマウントで、単一のパーティションのイメージをマウントする例は豊富にあったのですが、ディスク全体のイメージ中の各ファイルシステムにアクセスする方法がわかりませんでした。

色々と調べてmountやらlosetupのマニュアルを熟読したところ、offsetを指定して各ファイルシステムをマウントする方法があることが判明。ものはためしとやってみました。

offsetの調査

とりあえずoffsetを調べるためにディスクイメージをloopデバイスに丸々マウントします。まず使われていないloopデバイスを調べて…。

losetup -f

空いているloopデバイスに対して先ほどのイメージをマウント。これが成功すればloop0が/dev/hdbの中身を全部持っている状態になります。

losetup /dev/loop0 /root/hdd.img

loop0がディスクの内容を全部持っているため、fdiskを使うとパーティションテーブルに触れます。セクタ単位でパーティションの位置を見たいのでfdiskには-lではなく-luを与えます。

fdisk -lu /dev/loop0

結果は以下。パーティションの内容と開始位置がわかります。このうち僕がサルベージしたいのは1つめと4つめのNTFSパーティションです。

Disk /dev/hdb: 250.0 GB, 250059350016 bytes
255 heads, 63 sectors/track, 30401 cylinders, total 488397168 sectors
Units = sectors of 1 * 512 = 512 bytes

   Device Boot      Start         End      Blocks   Id  System
/dev/loop0p1   *          63   131973974    65986956    7  HPFS/NTFS
/dev/loop0p2       131973975   149966774     8996400   83  Linux
/dev/loop0p3       149966775   151958834      996030   82  Linux swap / Solaris
/dev/loop0p4       151958835   488392064   168216615    7  HPFS/NTFS

loopデバイスはlosetup -dでアンマウントします。

losetup -d /dev/loop0

今回はパーティションテーブルが読める状態でしたが、パーティションテーブルが破壊されている場合はgpartを使ってパーティションの開始位置を推定する方法もあるようです。

個々のパーティションのマウント

簡単にやるならmountコマンドのオプションにoffsetを指定して以下のようにします(先ほどの1つめのパーティションの場合)。

mount /root/hdd.img /mnt/ntfs -t ntfs -o ro,loop,offset=32256,nls=utf8

offsetが重要でパーティションの開始位置をバイト単位で指定します。パーティションの開始位置とはセクタサイズ512byteのハードディスクの場合、fdisk -luで出てくるStartにセクタサイズの512をかけた値になります(今回は32256=512*63)。

roは読み込み専用の指定です。loopはファイルをloopデバイスにマウントして扱う時に指定します。loop=/dev/loop0とか指定することもできますが空いているloopデバイスを勝手に使ってくれます。

またntfsパーティションをマウントする場合はnls=utf8を指定しないと日本語ファイルがLinuxから見えなくなります。utf8の指定方法にはiocharset=utf8とする方法や、-oとは別に-utfする方法がありますが、いずれも現在は推奨されずnlsを使うのが良いようです。

manに書かれている通りmountコマンドのoffset指定はlosetupのオプションです。恐らく-o loopを指定すると裏で空いているloopデバイスを探してそこへマウントした後、tオプションで渡したファイルシステムと解釈してloopデバイスをmountしてくれるのでしょう。

実際に試してみると以下の様にlosetupしてからmountしても同じ事ができます。

losetup /dev/loop0 /root/hdd.img --offset=32256
mount /dev/loop0 /mnt/ntfs -t ntfs -o ro,loop,nls=utf8

もしoffset指定があっているのにファイルシステムが云々といってマウントできない場合はファイルシステムのはじまりが壊れている可能性あります。場合によってはmountに-fを付けて強制マウントすると復旧できるかもしれません。

これでファイルシステムに触れますから、あとは生き残った内容を祈りながら眺めたり、必要に応じてcpでコピーするだけです。

感想

今回はパーティションテーブルが無事で、一部セクタが破損していただけでデータ内容もほとんどが無事という大変幸運なケースでした。万全を期してddでイメージを作成しましたが、Linuxからはディスクを直接マウントできた感じなのでddも不要だったかもしれません(ただ壊れたディスクをさわると症状が悪化する気もするのでddしたほうが心理的には大分楽だと思います)。

今回はNTFSなので為す術はありませんでしたが、もしファイルシステムext3などの場合はfsckでエラーの回復処理も可能です。正常な別のディスクにddで書き戻して、fsckで回復処理をすれば論理的な破損も修復できて、実機で触れるハードディスクを復活できるため便利そうですね。

MP4BoxでTimeScaleとDurationがおかしいmp4ファイルを修復する

ffmpeg(1年ほど前に試行錯誤でパッチを当てまくってビルドしたためバージョンは不明…)で下記オプションにてトランスコードしたファイルのTimeScaleとDulationが異常に大きな値になるという現象に遭遇しました。

ffmpeg -i target.m2p -f mp4 -vcodec h264 \
-deinterlace -r 29.97 -s 640x480 -b 720k \
-level 30 -qmin 12 -subq 6 -qmax 40 -g 250 \
-acodec aac -ac 2 -ab 128k -loop 1 target.mp4

プレイヤでは正常に再生することができるものの、異常に長いファイルだと認識されてしまい、時間外の場所にシークすると暴走してしまいます。

MP4Boxで調べてみるとビデオトラックのTimeScaleとDulationの値が非常に大きい値のファイルだということがわかりました。

* Movie Info *
       Timescale 1000 - Duration 00:30:28.971
       Fragmented File no - 2 track(s)
       File Brand isom - version 512

File has no MPEG4 IOD/OD

Track # 1 Info - TrackID 1 - TimeScale 29999970 - Duration 00:01:51.207
Media Info: Language "und" - Type "vide" - Sub Type "avc1" - 54821 samples
MPEG-4 Config: Visual Stream - ObjectTypeIndication 0x21
AVC/H264 Video - Visual Size 640 x 480
Self-synchronized

Track # 2 Info - TrackID 2 - TimeScale 48000 - Duration 00:30:28.970
Media Info: Language "und" - Type "soun" - Sub Type "mp4a" - 85733 samples
MPEG-4 Config: Audio Stream - ObjectTypeIndication 0x40
MPEG-4 Audio AAC Main - 2 Channel(s) - SampleRate 48000
Synchronized on stream 1

再生はできることからビデオのデータ自体はほぼ正常そうなことがわかっているので、正しいFPSを指定してやればちゃんとしたファイルになりそうです。

ffmpegX版mp4boxのfps指定方法 ageha was hereを参考にして、映像と音声のトラックを分離し、分離したトラックを再結合する方法で修復を試みました。

メタデータはトラックを分離するとついてこないため、結合時に正常なメタデータが付加されることを期待します。

MP4Box -raw 1 target.mp4
MP4Box -raw 2 target.mp4

これで各トラックが分離されトラック番号とフォーマットに応じたファイルがトラック毎に出現します。(target_track1.h264やtarget_track2.aacなど)

MP4Box -fps 29.97 \
-add target_track1.h264 \
-add target_track2.aac \
-new target.new.mp4

FPSを明示して結合。これでちゃんとしたmp4ファイルが作成されました。ツギハギなffmpegを使っている僕の環境に限った問題なような気がしますが、似たようにトラブッっているmp4ファイルなら同様に修正できるはず。

参考までに修復後のトラック情報は以下になります。

* Movie Info *
       Timescale 1000 - Duration 00:30:29.196
       Fragmented File no - 2 track(s)
       File Brand isom - version 512

File has no MPEG4 IOD/OD

Track # 1 Info - TrackID 1 - TimeScale 2997 - Duration 00:30:29.195
Media Info: Language "und" - Type "vide" - Sub Type "avc1" - 54821 samples
MPEG-4 Config: Visual Stream - ObjectTypeIndication 0x21
AVC/H264 Video - Visual Size 640 x 480
Self-synchronized

Track # 2 Info - TrackID 2 - TimeScale 48000 - Duration 00:30:28.970
Media Info: Language "und" - Type "soun" - Sub Type "mp4a" - 85733 samples
MPEG-4 Config: Audio Stream - ObjectTypeIndication 0x40
MPEG-4 Audio AAC LC - 2 Channel(s) - SampleRate 48000
Synchronized on stream 1

Debian etchにtrac-ja-10.4-1をインストールする

最近Tracを設置することが多いため最小インストールしたDebian etchで日本語化されたtrac-ja-10.4-1を動作させるまでの手順をメモします。

Trac本体を動作させるまで

Debian etchだと最小インストール状態から概ね以下のパッケージが必要になります。

インタアクト株式会社(http://www.i-act.co.jp/project/products/products.html)により日本語化されたtrac-ja-10.4-1を下記リンクから取得します。

http://www.i-act.co.jp/project/products/downloads/trac-0.10.4-ja-1.zip

アーカイブを解凍してsetup.pyを実行しTracをインストールします。

python setup.py install

Tracで参照するSubversionリポジトリを作成します(例えば/var/svn)。

svnadmin create /var/svn

Tracの環境を作成します(例えば/var/www/trac)。

trac-admin /var/www/trac/ initenv

trac-adminは対話的なコマンドなので適宜。基本的に「Path to repository」以外はデフォルトで問題はありません。

Path to repository > /var/svn/webjr

EGGで提供されるプラグインを利用する場合はEGGキャッシュ用のディレクトリを作成しておきます(例えば/tmp/python_egg_cache)。

続いてApacheの設定です。例えば適当なバーチャルホストの設定として以下を追加します。TracEnvとTracUriRootはそれぞれTracが存在するパスとアクセス時のURLを指定します。

<Directory /var/www/trac>
    SetHandler mod_python
    PythonHandler trac.web.modpython_frontend
    PythonOption TracEnv /var/www/trac
    PythonOption TracUriRoot /trac
    SetEnv PYTHON_EGG_CACHE /tmp/python_egg_cache
</Directory>

これでTracが動作する所まで設定が終わりました。Apacheを再起動してtracを設置したディレクトリにアクセスしてみましょう。

追加でプラグインをインストールする

Tracが動作することが確認できたら、代表的なプラグインであるTracWebAdminをインストールしてみます。

Trac公式サイト(http://trac.edgewall.org/wiki/WebAdmin)にある下記リンクから0.10.4向けのTracWebAdminのEGGを取得します。

http://trac.edgewall.org/raw-attachment/wiki/WebAdmin/TracWebAdmin-0.1.2dev_r6060-py2.4.egg.zip

trac-ja-10.4-1をそのままインストールすると/usr/share/trac/pluginsが全プロジェクト共通のプラグイン置き場になります。

拡張子.egg.zipから.zipを削除して.eggのみにした上で/usr/share/trac/pluginsに置いておきます。

mv TracWebAdmin-0.1.2dev_r6060-py2.4.egg.zip TracWebAdmin-0.1.2dev_r6060-py2.4.egg

これで全てのTracからTracWebAdminが利用可能になりました。それぞれのTrac環境でconf/trac.iniにcomponentsに関する記述を追加すると有効になります。

[components]
webadmin.* = enabled

あとは下記のようにtrac-adminを使いTracWebAdminにアクセスできる権限を与えたユーザを作成すれば良いです。

trac-admin /var/www/trac permission add hogehoge TRAC_ADMIN

もしeggを/usr/share/trac/pluginsではなく各Trac環境のpluginsに設置した場合はconf/trac.iniを設定しなくても自動的にプラグインが読み込まれるようです。

Retrospectiva公式サイトが復旧している模様

数日間アクセスできなくなっていたRetrospectiva公式サイトですが既に復旧しているようです。ただし復旧した公式サイトにもGoogle Codeでされたダウン報告にも、この件に関するアナウンスはまったく見当たらず、また1ヶ月以上コミットがない状況には変化がありません。やや不安ですが公式サイトが復旧して良かったです。