ラベル redmine plugin の投稿を表示しています。 すべての投稿を表示
ラベル redmine plugin の投稿を表示しています。 すべての投稿を表示

2012/08/11

録画リストビューにクエリー機能をつけてみた

チケットリストにビデオサムネイルが付く感じ?

前記事で録画チケットから視聴するためのリストビューをredmineプラグインで作ったけどオリンピックとかひたすら録画したら結構な数になった。そろそろ録画ビデオの検索機能が欲しくなった。
ということで、チケットをクエリ検索するのと同じ感覚の録画リストを作ってみた。

プロジェクトメニュー
今までのControllerやViewはそのままに、今回作ったプラグインビューはプロジェクトメニューで表示されるようにした。いろんなアプローチ方法があるって言うのが便利かなと思って。

init.rb
 # Project Menu
  menu :project_menu, :videos_show, { :controller => 'video', :action => 'show' },
  :caption => "Videos", :param => :project_id, :after => :overview
  permission :videos_show, {:video => :show}, :public => true

  Redmine::MenuManager.map :project_menu do |menu|
    menu.delete :activity
  end
こんな感じで、init.rbにプロジェクトメニューで、videoコントローラの'show'アクションが表示されるように。
そしてパーミッションは特に指定する必要がないんでパブリックです。
ついでに、活動(activity)メニューが邪魔なので消しました。
そこにVideosメニューが代わりに入る感じ。

活動メニューは仕事では結構便利だけど、この録画システムではすごい数のチケットが毎日登録されていて、その情報が表示されるとすごい時間がかかる。間違って押してしまうと暫く動かない。目的にそぐわないんで消してしまった。

改良版コントローラ
app/controllers/video_controller.rb
# -*- coding: utf-8 -*-                                                                                                           
class VideoController < ApplicationController
  menu_item :videos_show, :only => [:show]
  menu_item :videos_index, :only => [:index]
  menu_item :videos_view, :only => [:list]
  unloadable

  helper :queries
  include QueriesHelper
  helper :repositories
  include RepositoriesHelper
  helper :sort
  include SortHelper
  include IssuesHelper

  def index
    @issues = get_video_issues
  end

  def list
    # @issues = get_video_issues                                                                                                  
    @issue_pages, @issues = get_video_issues_paginate
  end

  def play
    @issue = Issue.find(params[:id])
  end

  def podcast
    @issues = get_public_issues
    render :layout => false, :content_type => 'text/xml'
  end

  def get_public_issues
    Issue.find(:all, :conditions => {:tracker_id => 3, :status_id => 6}, :order => "start_date DESC")
  end

  def get_video_issues
    Issue.find(:all, :conditions => {:tracker_id => 3, :status_id => 4}, :order => "start_date DESC")
  end

  def get_video_issues_paginate
    paginate(:issue, :per_page => 5,
             :conditions => {:tracker_id => 3, :status_id => 4}, :order => "start_date DESC")
  end

  def show
    @project = Project.find(params[:project_id])
    retrieve_query
    sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
    sort_update(@query.sortable_columns)

    if @query.valid?
      @limit = per_page_option
      @issue_count = @query.issue_count
      @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
      @offset ||= @issue_pages.current.offset
      @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
                              :order => sort_clause,
                              :offset => @offset,
                              :limit => @limit)
      @issue_count_by_group = @query.issue_count_by_group
    end

    #render :template => 'issues/index', :layout => !request.xhr?                                                                 
  end
end

前記事から書き加えたのは以下のような感じ。
  menu_item :videos_show, :only => [:show]
  menu_item :videos_index, :only => [:index]
  menu_item :videos_view, :only => [:list]
このmenu_item云々を付けておかないとメニュー押したときに選択状態にならないんだね。あとは、
  helper :queries
  include QueriesHelper
  helper :repositories
  include RepositoriesHelper
  helper :sort
  include SortHelper
  include IssuesHelper
と
  def show
の部分。

この辺りは、redmine/app/controllers/issues_controller.rbのindexアクションを参考に必要な部分だけにした感じ。
クエリ機能とついでにページ機能も付いた。

app/views/show.html.erb
app/views/_list.html.erb
ここは、
redmine/app/views/issues/index.html.erb
redmine/app/views/issues/_list.html.erb
をコピペして多少のパッチを当てただけなのでソースは載せません。
パッチした部分は以下の通り。

index.html.erb
<%= form_tag({ :controller => 'issues', :action => 'index', :project_id => @project },
を
<%= form_tag({ :controller => 'video', :action => 'show', :project_id => @project },
と
<%= render :partial => 'issues/list', :locals => {:issues => @issues, :query => @query} %>
を
<%= render :partial => 'video/list', :locals => {:issues => @issues, :query => @query} %>
あとはcsvとか別フォーマットで出力する場合のところや、atomのところとか、call_hookのところとかを削除して終わり。
最初はsidebarの追加コードを外してたんだけれど、付けてみたらすごい便利になった。
普通のチケットとして扱えて、録画ファイルが添付されているチケットはサムネイルが表示されて、クリックするとビデオ視聴できる。

_list.html.erb
テーブルヘッダのチェックボックスのところをコメントアウトして、テーブル本体を
<!--<td class="checkbox hide-when-print"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>-->
<td><% thumb = issue.attachments.detect {|a| a.content_type == "image/jpg"} %>
<% if !thumb.nil? %>
  <%= link_to image_tag("/videos/"+thumb.disk_filename), {:action => 'play', :id => issue.id } %>
<% end %></td>
こんな感じにリプレース。
image_tagでサムネイル表示して、playアクションへのリンクにした。

なんか、初めからこうすれば良かったってくらい馴染んだ感じになった。
検索できるし、ソートできるし、ページもあるし。

最初はよくわからないまま始めたんで要領が悪かったけど、少しずつ分かってくると、こうすれば楽じゃんっていう発想が出来るようになってくるもんだよね。

2012/07/29

録画ビデオのpodcast対応をredmineプラグインで

録画ビデオ視聴をredmineプラグインで出来るようにしたので、podcastも対応させる。
普通の録画ビデオフォーマットだとiPodTouch初代だと観れない問題を解決すべく、mpeg4フォーマットに更に変換したものをpodcastで観れるようにするというもので、
以前の記事でepgrecを対応させたが、便利だったのでredmineシステムでもやってみた。

前回は録画したものを何でもかんでもpodcastで出すようにしていたが、今回は録画が終了したものをredmine上で選択したものだけpodcastへ公開する形にした。

録画が正常終了したものはredmineステートで「完了」になるのだけど、新たに「公開」ステートを設けた。マニュアル操作で録画チケットを「公開」にしておくと、次の日にはpodcast上に出てくるという感じ。

そのステート監視と変換処理をするスクリプト
podcast.rb
#!/usr/bin/ruby
# -*- coding: utf-8 -*-
#
require 'rubygems'
require 'active_record'
require 'digest/md5'

@media_path = '/opt/videos/'

ActiveRecord::Base.establish_connection(
 :adapter => 'mysql2',
 :host => 'localhost',
 :username => 'redmine',
 :password => 'redmine',
 :database => 'redmine'
)

class Issue < ActiveRecord::Base
end
class CustomValues < ActiveRecord::Base
end
class Attachments < ActiveRecord::Base
end

def attach( desc, container_id, filename, type )
  if Attachments.first( :conditions => {
                          :container_id => container_id,
                          :digest => Digest::MD5.hexdigest(filename)
                        }).nil?
    Attachments.create( :container_id => container_id,
                        :container_type => "Issue",
                        :filename => filename,
                        :disk_filename => filename,
                        :filesize => File::stat( @media_path + filename ).size,
                        :content_type => type,
                        :digest => Digest::MD5.hexdigest(filename),
                        :author_id => 1,
                        :description => desc
                        )
  end
end

def start_date_time issue
  start_time = CustomValues.first( :conditions => {:customized_id=>issue.id, :custom_field_id=>1} )
  if start_time
    time = format("%04d", start_time.value.to_i)
    Time.parse(issue.start_date.strftime("%Y%m%d") + time)
  else
    Time.now
  end
end

# 公開ステータスの録画チケット
Issue.find( :all, :conditions => {:tracker_id => 3,:status_id => 6}).each {|issue|

  t_start = start_date_time issue
  filename = t_start.strftime("%Y%m%d%H%M")
  outfile = filename + '-p.mp4'

  if !FileTest.exists?( @media_path + outfile )
    # MPEG4
    if system '/opt/task/podcast.sh ' + @media_path + filename
      # アタッチ
      attach "MPEG4", issue.id, outfile, "video/m4v"
    end
  end
}
録画で公開ステータスになっているチケットを検索して、podcast用(mpeg4フォーマットのスモールバージョン)ビデオファイルが存在していなければ、作ってチケットにアタッチする処理。
ここでのポイント(微妙だけど・・)は、アタッチするファイルタイプを'video/mp4'ではなく、'video/m4v'にしたことかな。直接クリックして視聴できないんでタイプに意味はないけど、後でこれをキーワードにして検索したりするんで、他のアタッチファイルと区別できるようにしておく。

ipod-touch初代で観れるmpeg4ビデオを作成するシェルスクリプトはこちら
#!/bin/sh
OUT=$1
echo "OUTPUT  : $OUT"
ffmpeg -y -i ${OUT}.mp4 -threads 0 -s 480x272 -c:a copy -c:v mpeg4 -b:v 900k -qmin 3 -qmax 5 -f mp4 ${OUT}-p.mp4
この辺は前記事と変わりはないかな。ファイルの置き場所とかファイル名が違うくらい。

下回りはこんなでいいとして、podcastのGET部分をredmineのプラグインで簡単に組み込んでみた。この記事で書いたプラグインのビューとして作ってみた。

app/controllers/video_controller.rb に’podcast'関数を追加
  def podcast
    @issues = get_public_issues
    render :layout => false, :content_type => 'text/xml'
  end
  def get_public_issues
    Issue.find(:all, :conditions => {:tracker_id => 3, :status_id => 6}, :order => "start_date DESC")
  end

podcastはrss(xml)だ。xml出力をビューで処理したい。ということで、
render :layout => false; って書いておく。redmineのビューフォーマット全部をなしにしてくれる。

ビューの部分 app/views/video/podcast.html.erb を記述
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" version="2.0">
  <channel>
    <title>公開ビデオ</title>
    <language>jp</language>
    <itunes:category text="録画">
      <itunes:category text="TV録画"/>
    </itunes:category>

    <% @issues.each do |issue|
       video = issue.attachments.detect {|a| a.content_type == "video/m4v"}

       if video
       channel = issue.custom_values.detect {|v| v.customized_id == issue.id and v.custom_field_id == 2 }
       pubdate = video.filename.sub(/\..*/,'')
       %>
    <item>
      <title><%= issue.subject %></title>
      <enclosure url="http://<%= request.host + path_to_video(video.disk_filename) %>" type="video/mp4"/\
>
      <pubDate><%= pubdate %></pubDate>
      <itunes:author><%= channel.value %></itunes:author>
      <itunes:summary><%= issue.description %></itunes:summary>
      <itunes:category><%= issue.category %></itunes:category>
      <itunes:duration><%= issue.estimated_hours %></itunes:duration>
    </item>
    <% end %>
    <% end %>

  </channel>
</rss>
Railsのviewsになっただけで、以前Smartyテンプレートで記述したのとほとんど同じ。前回は内部処理をPHPで記述していたが、今回はrubyだ。(今回はペースト成功した!)

config/routes.rb に忘れないように以下の1行を追加
get 'video/podcast',:to=>'video#podcast'                                                                       
iTunesからhttp://(redmine-home)/video/podcast をpodcast登録しておく。
前回悩んだguidタグだけど、記述しなくても大丈夫そうだったのでなしにした。

オリンピック録画中なので、あまり大きな変更修正で失敗しない程度の拡張でした。

2012/07/24

video_tagで視聴するredmineプラグインを作る

redmineチケット予約・録画までは出来たが、視聴する部分をどうするか悩んだ結果、
致し方なくじゃなくて、頑張ってredmineプラグインで視聴する部分を作成してみた。

redmineはrailsアプリなので、railsとしてのMVCのVC(View & Controller)を使って簡単に作れるんじゃないかな?と思った。だけどrailsのちゃんとしたプラグインは作ったこと無い。調べながら作って何とかなったので、工程を書き記しておく。

/opt/redmine
ここが自分のredmineのインストール場所。人によって色々だよね。
ここをカレントディレクトリにしてプラグイン生成を行う。

自分のRedmine環境
  Redmine version                          2.0.3.stable
  Ruby version                             1.9.3 (x86_64-linux)
  Rails version                            3.2.6
  Environment                              production
  Database adapter                         Mysql2

プラグイン作成
$ sudo RAILS_ENV=production ruby script/rails generate redmine_plugin redmine_video
RAILS_ENV環境変数を指定しないと
var/lib/gems/1.9.1/gems/bundler-1.1.4/lib/bundler/rubygems_integration.rb:147:in `block in replace_gem': Please install the mysql adapter: `gem install activerecord-mysql-adapter` (mysql is not part of the bundle. Add it to Gemfile.) (LoadError)
とかいうエラーが出る。これを信じるとハマる。

何か昔のRedmineとは作成フレーズが違うのだね。
redmine色が薄れてRails色が濃くなった感じ?
以前はプラグイン名のプレフィックスに勝手に'redmine_'が追加されたような・・・
最新のはそうじゃないみたいだけど、過去の慣習に合わせて'redmine_video'にした。
これも安易な名前だけど、自分用なのでこれでよし。

コントローラ作成
$ sudo RAILS_ENV=production ruby script/rails generate redmine_plugin_controller redmine_video video index
コントローラテンプレートもひとつ作る。アクションは一応'index'だけ作っておく。
追加はコントローラスクリプトに直接書き加えて追加していけばいいか。

/opt/redmine/plugins/redmine_video
プラグインがここに作られた。以前は/opt/redmine/vender/plugins/だったはず。
場所変わった。とにかくここに色々作られるけど、使うのはapp/controllersとapp/viewsだけだろう。

config/routes.rb
これもちゃんと書かないとダメってことになったらしい。勝手には作ってくれないので自分で書き加える。最初は以下の一行だけ書いておく。後からアクションが増えるたびに書き加える。
get 'video/index',:to=>'video#index'
えーと、http://(redmine-url)/video/indexってやったら、videoコントローラのindexアクションがGETメソッドで呼ばれるっていうルート定義。勘違いしていた。アクセスがGETメソッドに限定されるルート定義っていう意味だった。
POST限定なら post、GET/POST両方で使うなら match らしい。

app/controllers/video_controller.rb
この中にvideoコントローラのアクションの内部処理を記述する。
以下のように、indexアクションで録画チケットを検索してViewに渡すようにした。
def index
  @issues =Issue.find(:all, :conditions => {:tracker_id => 3, :status_id => 4}, :order => "start_date DESC")
 end

app/views/video/index.html.erb
コントローラ内でrenderで表示する処理を書かなければ、controller名/アクション名_html.erb
がテンプレートとして表示されるようになる。この中にもrubyスクリプトを埋め込める。試行錯誤の末、出来上がったのがこちら。
<h2>録画ビデオ一覧</h2>

<table class="list">
    <% @issues.each do |issue| %>

    <tr class="<%= cycle('odd','even') %>">
      <td>
        <% thumb = issue.attachments.detect {|a| a.content_type == "image/jpg"} %>
        <%= link_to image_tag("/videos/"+thumb.disk_filename),
            {:action => 'play', :id => issue.id } %>
      </td>
      <td>
        <table>
        <% time = issue.custom_values.detect {|v|
           v.customized_id == issue.id and v.custom_field_id == 1 }
           channel = issue.custom_values.detect {|v|
           v.customized_id == issue.id and v.custom_field_id == 2 } %>
        <tr><th nowrap>タイトル:  <th><b><%= issue.subject %></tr>
        <tr><td nowrap>ID:         <td><%= issue.id %></tr>
        <tr><td nowrap>説明:      <td><%= issue.description %></tr>
        <tr><td nowrap>録画日:    <td><%= issue.start_date %></tr>
        <tr><td nowrap>時間:      <td><%= format("%04d",time.value.to_i) %></tr>
        <tr><td nowrap>チャンネル:<td><%= channel.value %></tr>
        <tr><td nowrap>カテゴリ:  <td><%= issue.category %></tr>
        </table>
      </td>
      <% end %>
</table>
(貼り付けが綺麗にできないので、'<'を"<"にしてます)

<% @issues.each do |issue| %>~<% end %>
コントローラから受け取った@issues分繰り返し。<%と%>で囲ったところがrubyスクリプトとして実行される。何処にでも挿入できる。

<% thumb = issue.attachments.detect {|a| a.content_type == "image/jpg"} %>
サムネイル表示どうしようかと重たけど、チケットにアタッチしてあるやつをこんな風に取得できた。これがそのままattachementクラスなので、thumb.disk_filenameでファイル名が取り出せる。

この辺が外部スクリプトでActiveRecord使ってアクセスしてた時と勝手が違うところだね。

image_tag(filepath)
railsの関数。これ使うと、<img src="filepath" />に展開してくれる。filepathをファイル名だけにすると、"images/filename"に勝手になる。

imagesディレクトリはredmineが使用済みなので、'/videos'でファイルにアクセスできるようにした。

最初は、<img src="/redmine/attachments/<%= thumb.id %>/<%= thumb.filename %>
なんて書いてみたけれど、ものすごく遅いので、/opt/redmine/public/videos/にビデオやサムネイルファイルが保存してある場所へリンクを貼ることにした。
'videos'にしたのは、後で出てくる、video_tagのデフォルトパスになっているから。

link_to
railsの関数。これはアンカータグに変換してくれる。
{:action => 'play', :id => issue.id }
同じコントローラ内のplayアクションにid付きのコマンドへリンクするよってこと。

平たく言うと、サムネイルをクリックすると、video/play/#チケット番号 へ飛ぶリンクが作られるようにってことだ。結果は簡単だけど知らないところからやると結構悩むよ。むずいよ。

開始時間とかのカスタムフィールドもattachmentsと同じ要領で取得できたけど、何かカッコ悪い感じがする。もっと簡素に書けるんじゃないかなぁ。でもRails初心者なのでここまでが精一杯だ。

(redmine-url)/video/index で一覧表示!
録画チケットを、よくあるサムネイル付きの一覧表示させる事ができました。

playアクション
一覧表示のリンクから呼ばれる再生用のビューを作成します。

routes.rbに、get 'video/play/:id',:to=>'video#play' を追加する。
video/play/#id でアクセスしたら、videoコントローラのplayアクションが呼ばれる。
#idは、コントローラスクリプト内でparams[:id]で参照できる。なので、
def play
 @issue = Issue.find(params[:id])
end
っていう簡単な処理をvideoコントローラに追加した。@issueに目的のチケットが格納される。

app/views/video/play.html.erb
index.html.erbと同じ所にplay.html.erbを作る。generateで作らなかったので自分で作る。
<h2>録画ビデオ一覧</h2>
<h2><%= @issue.subject %></h2>

<% video = @issue.attachments.detect {|a| a.content_type == "video/mp4"} %>
<% thumb = @issue.attachments.detect {|a| a.content_type == "image/jpg"} %>

<%= link_to video_tag video.disk_filename,
    :poster => image_path("/videos/"+thumb.disk_filename),
:autoplay => true, :autobuffer => true, :controls => true, :size => "960x540" %>
(こっちも、'<'を"<"にしてます)

video_tag
肝心の関数です。video_tagというrails関数を使うと、
video_tag video.filename しか書いてないけど、これで、<video src="/video/filename"> に変換される。アトリビュートもオプション設定で幾つかできるので、適当に設定してみた。

フルスクリーン設定ができない
大体のブラウザでフルスクリーンにできるから大した問題ではないけど、出来ないやつもあるんで、一応大きめのサイズを設定しておく。この辺は別の手段で色々やればなんか出来たりするんじゃないかと思うけど、分からんです。
試しに、:size="100%x100%"とかやってみたけど、やっぱりダメだった・・・

MacやPCだとまあまあ良い感じ
一覧からサムネイルクリックでビデオ再生ページになって自動再生されるように出来た。デスクトップ環境からなら私はこれで十分OKだ。ぜんぜん使える!

だけど、Firefoxだと再生しないぞ?なんででしょう?
何でだ?じゃなくて、Firefoxはogg(theora+vorbis)じゃないと再生できないんだね。最近知りましたよ。そういえばffmpegのコンフィグでそんなのがあったな。これも今後の課題かな。

iPad/Androidなどのモバイル端末だとなぁ
前にも書いた気がするけど、autoplayが効かないのはそのままなので、再生ページで自分で小さな再生ボタンをタッチしなければならない。で、そのページ内で再生されるわけじゃなくビデオプレーヤが起動してその中で再生されるという、まだるっこしい感じになる。

一覧にvideoタグが付いていればいいんじゃね?
と思ってやってみた。一覧のサムネイルにビデオ再生ボタンが付いた感じになった。
心なしかこっちの方が再生ボタンが大きい気がする。モバイル端末ならば、勝手にフルスクリーンで再生されるから、この方が自然な感じがする。
逆に、デスクトップのブラウザだと小さいサムネイルサイズのまま再生されるので、再生ページに飛ばしたほうが自然だ。

うーむ。どっちもどっちだ。ということで、両方ありにした。
視聴する環境で、どっちがいいか自分で選択すればいいじゃないかと。
ということで、選択しやすいメニューを追加することにした。

listアクションとlist.html.erb
コントローラアクション'list'を、indexと同じ処理で追加。
routes.rbへ、get 'video/list',:to=>'video#list' を追加。
index.html.erbのlink_toの部分を以下のように変更しただけのビューを追加。
<%= video_tag video.disk_filename,
       :poster => image_path("/videos/"+thumb.disk_filename),
       :controls => true, :size => "320x180" %>

分かりにくいけど、video#indexがPCに適したやつ。video#listがモバイルに適したやつ。アクションビュー2種類にしたので、この2つを選択するためのメニューを追加する。

アプリケーションメニュー
redmineプラグインメニューはいろんな所に出現させることが出きるようだけど、一番簡単そうでタッチしやすい(モバイル端末だとリンクが小さいと押しにくいのだ)、アプリケーションメニューに追加することにした。

app/init.rb
この中で何処にメニューを出すかとか、パーミッションとかグループとか色々やるらしいけど、アプリケーションメニュー設定は簡単だ。

menu( :application_menu,
      :videos_index, { :controller => 'video', :action => 'index' }, :caption => "VideoIndex" )
menu( :application_menu,
      :videos_view, { :controller => 'video', :action => 'list' }, :caption => "VideoList" )

をdo/endの中に追加すればいいだけ。カッコは省略するほうがRubyOnRailsらしいそうだけど、自分は癖で書いてしまった。

ruby/rails/RailsGuidesをゆっくり和訳してみたよ
プラグイン開発ではこちらのサイトを大いに活用させていただきました。

予想していたよりも遥かに簡素に書けることが分かった。が、そのシンプルな結果を得るまでに苦労する。便利フレームワークは便利になるまでが苦しい。

完成にはまだ至らないけれど、番組データ取得・予約・録画・視聴までの一連のフローは実現できたかな。オリンピックにぎりぎり間に合ってよかった~。

色々実用レベルでの問題を残しっぱなしなので次回からは、そうした細々したところを1つ1つ解決していく感じになるかな。

因みに、不完全ながらもオリンピック番組を録画するために、クエリー使った自動予約機能を使う。重複処理ができてないので、1チャンネルだけにしてかつ、ライブじゃなくて録画番組の方が予約されるように設定した。うまく処理されるかなぁ?