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の他にoggやflacもまとめて扱えるようだったので、これを使いました。
…と、万事これでうまくいくかと思いきや、ruby-audioinfo内部で呼んでいるruby-mp3infoもid3tagの文字コード関係は鬼門で、調べてみるとid3v2は実質的にutf-8しか扱えないようです。結局ファイルからタグを読んだ時点で強引にkconvでutf-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並みとはいわなくても、特に補完はもう少しがんばって欲しいところです。ざっと調べたところvimでJava向けの強力な補完を利用するには、今のところ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
使い方
オムニ補完はデフォルトで
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
壊れたかもしれないハードディスクからのデータサルベージ
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だと最小インストール状態から概ね以下のパッケージが必要になります。
- 基本的に必要なパッケージ
- Apache関連(mod-python動作時)
- apache2
- libapache2-mod-python
- Tracの動作に必要なパッケージ
- python-pysqlite2
- python-subversion
- python-clearsilver
- EGGプラグイン利用に必要なパッケージ
- python-setuptools
インタアクト株式会社(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ヶ月以上コミットがない状況には変化がありません。やや不安ですが公式サイトが復旧して良かったです。