2012/08/18

DBにタイムラインテーブル構築

この録画システム用にカスタムDBテーブルを作ることにした。
MySQLとかよく知らないんで、作成はRedmine(Rails)のモデル作成スクリプトで。
アクセスはActiveRecordとかRedmineプラグインから。SQL文は書きません。

狙いは
  • かねてから問題だった番組情報の更新がうまく行かず番組チケットが重複してしまう問題の解決手段
  • 処理速度改善
  • 動的スケジュールの下地(将来機能)
日々取得するEPG情報の9割弱が既存番組情報
EPG情報は一週間分の情報が含まれている。新しい番組情報は一週間後の1日のものがほとんど。それ以外は既存の番組情報。厄介なのは一週間の間にプログラムが色々変更されたりすることだ。

今までは番組名や説明文に変更があれば更新する程度の対応はしていた。
それ以外の放送時間とかは変更されない。というか変更に対応できていなかった。
そのため大幅な変更があった場合は新規番組として認識して同じ番組が重複していた。
それが自動予約対象だったりすると、もう大変。手動でお掃除である。

オリンピック番組がよいサンプルになった
番組枠はあっても、内容がめっちゃ変わる。終了時間が1時間変更されたり、ジャンルが変わったり。もう番組の変更とは言えないくらい変わった。
こういうスペシャル番組が特に自分が残したい対象なのでどうにかしたい。

そろそろ本題
今までRedmineのモデルだけで何とかしてきたけれど、時間を扱うカスタムフィールドを作ることが出来ない。仕方なく自前のテーブルで対応することにした。Redmineプラグインで視聴や予約処理をする部分を作っているので専用モデル作っても大変じゃなさそうだし。

タイムラインテーブルを作ることにした
録画時間がRedmine既存モデルで日付と時間に分かれているが、検索に時間がかかるのと、今後のことも考えて日時を扱うシンプルなレコードを用意して、検索時間の短縮とシンプル化を計る。同時に大幅な変更にもある程度耐えられるような検索が行えるように。

ここから本題(前置き長くてすみません)

もちろんRedmineプラグインでモデルを作ったことがなかったんで試行錯誤した。その辺のこともちょっと書いておくよ。巷のハウツー情報はRedmine2.0以前の物が多く、手順が違ったりして最初は結構戸惑ったよ。

テーブル内容
issue_id : チケット番号
start_on : 開始日時
end_on : 終了日時
channel : チャンネル
reserved : 予約?

後からstart_on,end_onとかカッコつけずにstart_time,end_timeとかにすればよかったと後悔することになったけど、作ってしまったのでそのままである。

モデルテンプレート作成
$ sudo RAILS_ENV=production ruby script/rails generate redmine_plugin_model redmine_video timeline issue_id:integer start_on:datetime end_on:datetime channel:string reserved:boolean

すると、以下の様にテンプレートスクリプトファイルが作成される。
/var/lib/gems/1.9.1/gems/activesupport-3.2.6/lib/active_support/dependencies.rb:251:in `block in require': iconv will be deprecated in the future, use String#encode instead.
create plugins/redmine_video/app/models/timeline.rb
create plugins/redmine_video/test/unit/timeline_test.rb
create plugins/redmine_video/db/migrate/001_create_timelines.rb

db/migrate/日付_create_timelines.rb ってなるから001_に直せという記事があるが、最近のRedmineでは001になるらしい。

やり直したかったら
$ sudo RAILS_ENV=production ruby script/rails d redmine_plugin_model redmine_video timeline

で生成したテンプレートを削除出きる。

'd'はdestroyの省略形。

$ ruby script/rails
でどんなコマンドがあるか確認できる。因みにgenerateは'g'。

レコード名が気に入らなかったので
$ sudo RAILS_ENV=production ruby script/rails g redmine_plugin_model redmine_video video_timeline issue_id:integer start_on:datetime end_on:datetime channel:string reserved:boolean

で、以下のファイルが作成された。
/var/lib/gems/1.9.1/gems/activesupport-3.2.6/lib/active_support/dependencies.rb:251:in `block in require': iconv will be deprecated in the future, use String#encode instead.
create plugins/redmine_video/app/models/video_timeline.rb
create plugins/redmine_video/test/unit/video_timeline_test.rb
create plugins/redmine_video/db/migrate/002_create_video_timelines.rb

ん?002_create_video_timelines.rb? なんで002なんだ? どうしてだか、001しかないのに002を消したと言っている。実際001が残ってしまっている。手動で消したよ。
気持ち悪いので、最初からやり直して、001が作られるようにした。

DBマイグレーション
$ sudo rake db:migrate:plugin NAME=redmine_video RAILS_ENV=production

じゃなくて、最近は
$ sudo rake redmine:plugins:migrate NAME=redmine_video RAILS_ENV=production

だそうだ。これで実際にDBにレコードが作成される。

失敗したら
$ sudo rake redmine:plugins:migrate NAME=redmine_video RAILS_ENV=production VERSION=0

ってやればテーブルがドロップされる。

RAILS_ENVとか環境変数は、前につけたり後につけたりしてるけど、rubyなどのバイナリツールの場合は前に、rake等シェルツールの場合は後指定って感じかな?

DBを確認してみる
$ mysql -uredmine -predmine redmine
mysql> show tables;
video_timelinesってのができた。
mysql> show columns from video_timelines;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int(11)      | NO   | PRI | NULL    | auto_increment |
| issue_id | int(11)      | YES  |     | NULL    |                |
| start_on | datetime     | YES  |     | NULL    |                |
| end_on   | datetime     | YES  |     | NULL    |                |
| channel  | varchar(255) | YES  |     | NULL    |                |
| reserved | tinyint(1)   | YES  |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+
うん。できているようだ。
こんな風に色々試行錯誤しながら進めた。自然と作ったり消したりの作業が気安い感じになってくる。

下準備
これからタイムラインベースの検索をしたいので既存番組チケットにタイムラインレコードを作成する。

まずはお掃除(clean.rb)
#!/usr/bin/ruby
# -*- coding: utf-8 -*-
require 'rubygems'
require 'active_record'
require 'active_support/core_ext'

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 IssueCategories < ActiveRecord::Base
end

#
# ダブった番組チケットができてしまうので、トラバースしてダブリを削除する
#
del_issues = []
issues = Issue.find(:all, :conditions => {:tracker_id => 1,:status_id => 1})
issues.each {|issue|
  time = CustomValues.first( :conditions=> {
                               :customized_type => "Issue",
                               :customized_id => issue.id,
                               :custom_field_id => 1 }).value
  # チャンネル
  ch = CustomValues.first( :conditions => {
                             :customized_type => "Issue",
                             :customized_id => issue.id,
                             :custom_field_id => 2 }).value

  same_date_issues = Issue.find(:all, :conditions =>{
                                  :tracker_id => 1,
                                  :status_id => 1,
                                  :start_date => issue.start_date,
#                                  :estimated_hours => issue.estimated_hours,
#                                  :category_id => issue.category_id})
                                })

  next if same_date_issues.size <= 1
#  puts '%d %d' % [issue.id, same_date_issues.size]

  same_date_issues.each {|same|
    if same.id != issue.id
      same_time = CustomValues.first( :conditions=> {
                                        :customized_type => "Issue",
                                        :customized_id => same.id,
                                        :custom_field_id => 1 }).value
      same_ch = CustomValues.first( :conditions => {
                                      :customized_type => "Issue",
                                      :customized_id => same.id,
                                      :custom_field_id => 2 }).value
      if same_time == time and same_ch == ch and same.id > issue.id
        p 'find same ticket %d => %d' % [issue.id, same.id]
        del_issues <<= same.id
      end
    end
  }
}

puts 'deleted %d issues' % del_issues.size
Issue.delete(del_issues)

values = (del_issues.collect {|i| CustomValues.find(:all,:conditions=>{ :customized_id=>i })}).flatten

puts 'deleted %d custom values' % values.size
CustomValues.delete(values)

cate = IssueCategories.select {|c| not Issue.exists?(:category_id => c.id) }
puts 'deleted %d independency categories' % cate.size
IssueCategories.delete(cate)

今までの重複したチケットをできる限り認識して削除しておく。総当りに近いのでものすごい時間がかかった。

タイムライン作成(timeline.rb)
#!/usr/bin/ruby
# -*- coding: utf-8 -*-

require 'active_record'

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 VideoTimelines < ActiveRecord::Base
  # :issue_id : integer
  # :start_on : datetime
  # :end_on   : datetime
  # :channel  : string
  # :reserved : boolean
end

#
# Timeline登録
#
Issue.find(:all, :conditions=> ["status_id<3"]).each {|issue|
  unless VideoTimelines.exists?(:issue_id => issue.id)

    channel = CustomValues.first( :conditions => {
                                    :customized_id => issue.id,
                                    :custom_field_id => 2}).value
    time = CustomValues.first( :conditions => {
                                 :customized_id => issue.id,
                                 :custom_field_id => 1}).value.to_i

    start_on = Time.parse( issue.start_date.strftime("%Y%m%d") + format("%04d",time) )
    end_on = start_on + issue.estimated_hours.to_i * 60

    timeline = VideoTimelines.create( :issue_id => issue.id,
                                      :start_on => start_on,
                                      :end_on => end_on,
                                      :channel => channel,
                                      :reserved => issue.tracker_id == 2
                                      )

    puts "%d %d %s %s %s %s" % [timeline.id,
                                timeline.issue_id,
                                timeline.start_on,
                                timeline.end_on,
                                timeline.channel,
                                timeline.reserved.to_s]
  end
}

既存番組チケットに紐付いたタイムラインレコードを作成する。
一度実行するだけの単発スクリプトだけど、実際には試行錯誤する上で何度も実行することになるんで、落ち着くまでは大活躍である。

間違ったりうまく行かなかったときは、SQLコマンドで直接削除してから再実行したりする。
なんだかんだと色々やって毎日のタスクが出来上がった。(まだ発展途上だけど)

修正版 entry2.rb
#!/usr/bin/ruby
# -*- coding: utf-8 -*-
require 'rubygems'
require 'active_record'
require 'rexml/document'

ActiveRecord::Base.establish_connection(
                                        :adapter => 'mysql2',
                                        :host => 'localhost',
                                        :username => 'redmine',
                                        :password => 'redmine',
                                        :database => 'redmine'
                                        )
class Issue < ActiveRecord::Base; end
class IssueCategorie < ActiveRecord::Base; end
class CustomValue < ActiveRecord::Base; end
# VideoTimelines 独自レコード
class VideoTimeline < ActiveRecord::Base; end

$entry_count = 0
$update_count = 0
$update_time_count = 0

def create_job id
  # not yet
end

def delete_job id
  # not yet
end

def delete_issue id
  issue = Issue.find(id)

  # 予約実行中?
  delete_job id if issue.tracker_id == 2 and issue.status_id == 2

  CustomValue.delete_all(:customized_id => id)
  VideoTimeline.delete_all(:issue_id => id)

  Issue.delete(id)
  puts "deleted #{id}"
end

def create_issue info

  # 日にちと時間に分離
  start_date = info[:start_time].to_date
  start_time = info[:start_time].strftime("%H%M").to_i

  # チケット作成
  issue = Issue.create( :project_id => 1, #'番組プログラム'
                        :tracker_id => 1, #'番組'
                        :lft => 1,
                        :rgt => 2,
                        :status_id => 1,
                        :priority_id => 2,
                        :done_ratio => 0,
                        :author_id => 1,
                        :subject => info[:title],
                        :description => info[:desc],
                        :start_date => start_date,
                        :due_date => start_date,
                        :estimated_hours => info[:duration],
                        :category_id => info[:category_id] )

  # root_id 更新
  # NULLのままだとRedmineからのチケット更新でクラッシュする
  # 親がない場合は自分のIDを入れるらしい。自分? 更新しか無い?
  issue.root_id = issue.id

  if issue.save
    # 開始時間
    CustomValue.create( :customized_type => "Issue",
                        :customized_id => issue.id,
                        :custom_field_id => 1, #'開始時間'
                        :value => start_time )
    # チャンネル
    CustomValue.create( :customized_type => "Issue",
                        :customized_id => issue.id,
                        :custom_field_id => 2, #'チャンネル'
                        :value => info[:channel] )
    # 内容
    CustomValue.create( :customized_type => "Issue",
                        :customized_id => issue.id,
                        :custom_field_id => 4, #'内容'
                        :value => info[:desc] )
    # descriptionをフィルタできないから。出来るフィールドを作った
  end

  # タイムライン作成
  VideoTimeline.create( :issue_id => issue.id,
                        :start_on => info[:start_time],
                        :end_on => info[:end_time],
                        :channel => info[:channel],
                        :reserved => false )

  $entry_count = $entry_count + 1
  puts "NEW %s" % issue.id
end

def update_issue timeline, info

  issue = Issue.find(timeline.issue_id)

  update_f = false
  update_time_f = false

  # 題名
  if issue.subject != info[:title]
    issue.subject = info[:title]
    update_f = true
  end
  # 内容
  if issue.description != info[:desc]
    issue.description = info[:desc]
    content = CustomValue.first(:conditions => {
                                  :customized_type => "Issue",
                                  :customized_id => issue.id,
                                  :custom_field_id => 4 })
    if content
      content.value = info[:desc]
      content.save
    end
    update_f = true
  end
  # カテゴリ
  if issue.category_id != info[:category_id]
    issue.category_id = info[:category_id]
    update_f = true
  end

  # 時間
  if issue.estimated_hours != info[:duration]
    issue.estimated_hours = info[:duration]
    update_f = update_time_f = true
  end

  # タイムライン
  if timeline.start_on != info[:start_time]
    timeline.start_on = info[:start_time]
    update_f = update_time_f = true

    # 録画時間
    start_time = timeline.start_on.strftime("%H%M").to_i
    time = CustomValue.first( :conditions=> {
                                :customized_type => "Issue",
                                :customized_id => issue.id,
                                :custom_field_id => 1 })
    if time
      time.value = start_time
      time.save
    end
  end

  if timeline.end_on != info[:end_time]
    timeline.end_on = info[:end_time]
    update_f = update_time_f = true
  end

  # 更新
  if update_f
    issue.save

    # 更新 timeline
    if update_time_f
      timeline.save

      # 予約実行中?
      if issue.tracker_id == 2 and issue.status_id == 2
        delete_job issue.id
        create_job issue.id
      end

      $update_time_count = $update_time_count + 1
      puts "UPDATE TIME %d" % issue.id
    end

    $update_count = $update_count + 1
    puts "UPDATE %d" % issue.id
  end
end

#
# チケット登録
#
def entry_issue info

  # マッチするチケットを検索

  # 5分前後の変動を許容する
  range = 5 * 60
  startL = info[:start_time] - range
  startR = info[:start_time] + range
  endL = info[:end_time] - range
  endR = info[:end_time] + range

  timelines = VideoTimeline.where(:channel => info[:channel],
                                  :start_on => startL..startR,
                                  :end_on => endL..endR)
  # 大幅な終了時間変更?
  timelines = VideoTimeline.where(:channel => info[:channel],
                                  :start_on => info[:start_time]) unless timelines.size==0

  if timelines.size>=1
    if timelines.size==1
      #特定された
      update_issue timelines.first, info
      return
    else
      #複数あった!もうなんだかわからないんで全消し
      timelines.each {|vt| delete_issue vt.issue_id }
    end
  end

  # 大幅な変更があると見つけるのが困難なので
  # 何がどう変わったかをちゃんと判別するのはやめて、昔のは消して新規にする

  # かぶっている番組があれば、削除する(消滅した?)
  VideoTimeline.where("channel = ? and start_on >= ? and end_on <= ?",
                      info[:channel], info[:start_time], info[:end_time]).each {|vt|
    delete_issue vt.issue_id
  }

  # 新規に作る
  create_issue info

end # entry_issue

#
# TVプログラム登録
#
xml_file = ARGV.shift + '.xml'
xml = REXML::Document.new open(xml_file).read

ch_name = xml.elements['tv/channel/display-name'].text

xml.elements.each('/tv/programme') {|program|

  start = Time.parse( program.attributes['start'] )
  stop = Time.parse( program.attributes['stop'] )
  duration = (stop - start) / 60

  # 5分以下の短い番組は登録しない :todo: DBに設定できると良い
  next if duration <= 5

  # カテゴリ
  category = program.elements.to_a('category').collect{|c| c.text}.join(".")
  cate = IssueCategorie.where(:project_id => 1, :name => category).first
  cate = IssueCategorie.create( :project_id => 1, :name => category ) if cate.nil?

  info = {
    :duration => duration,
    :start_time => start,
    :end_time => stop,
    :channel => program.attributes['channel']+ '.'+ch_name,
    :title => program.elements['title'].text,
    :desc => program.elements['desc'].text,
    :category_id => cate.id
  }

  info[:desc] = "" if info[:desc].nil?
  info[:title].gsub!(/(【二】|【デ】|【S】|【字】|【SS】|【手】|【解】)/,"")

  entry_issue info
}

puts "%d entried" % $entry_count
puts "%d updated" % $update_count
puts "%d time updated" % $update_time_count
epgdumpの情報を読んで番組チケット更新&登録処理を行う部分。以前はXMLをHashに変換していたけど、今回はREXMLを使ってみた。Rails3の新しい書き方とかActiveRecordの新しく知った書き方とかもあるんで、そんなのを意識的に使って書いてみた。

ここで気づいたんだけれど、
class VideoTimeline < ActiveRecord::Base; end
とか、テーブル名を単数形にしている。実際のテーブル名は'video_timelines'で複数形。おや?と思ってどっちも書いてみたけど単数形でも複数形でもどっちでも大丈夫だった。
Redmineのテーブルやカスタムフィールドテーブルも同じ。なのでいままで複数形で書いていた部分もわざと単数形にしてみた。ふむ。普通に動く。不思議だ。

新たに知った範囲検索
深夜番組とか時間が微妙にずれたりする場合があるんで、時間を範囲で検索したい。どうするんだ?と思ったら
:start_on => L..Rって簡単に書けた!素晴らしいです。

複雑な場合分け検索はやめてありがちな変更だけに絞る。それ以外は諦めて、古いやつを消して新しく作る。そんな考えで作って処理を簡素化した。実際これで結構いけているようだ。

Jenkinsとの連携はまだ
既に予約実行中の番組に時間変更があった場合、Jenkins上のジョブも更新する必要があるだろ。そう思って関数は用意したけど、今のところその必要性が無いのと、今後の対応で思うところがあって空白のままだ。将来の動的スケジュール機能とセットで考えようと思ってる。

自動予約も更新(query2.rb)
#!/usr/bin/ruby
# -*- coding: utf-8 -*-

require 'active_record'
require 'rest_client'
require 'hudson-remote-api'

key = '???'
api = "http://localhost/projects/%s/issues.xml?key="+key+"&query_id="

# レコード
class Issue < ActiveRecord::Base; end
class CustomValues < ActiveRecord::Base; end
class Query < ActiveRecord::Base; end
class Project < ActiveRecord::Base; end
# VideoTimelines
class VideoTimelines < ActiveRecord::Base; end

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

# project識別名を合成
api = api % Project.first(1)[0].identifier.to_s

# Jenkins接続
Hudson.settings = {:url => 'http://localhost/jenkins', :crumb => false }

# ジョブ作成
def create_job info

  # Config
  return false if !Hudson::Job.list.include?("record_0")
  config = REXML::Document.new(Hudson::Job.get("record_0").config)

  # コマンド定義
  commands = [
              "/opt/task/ready.rb %d" % info[:issue_id],
              "/opt/task/record2.sh %d %d %s" % [
                                                 info[:channel],
                                                 info[:duration],
                                                 info[:output]
                                                ],
              "/opt/task/record2.rb %d" % info[:issue_id]
             ]

  # 時間設定
  config.elements["/project/triggers/hudson.triggers.TimerTrigger/spec"].text =
    "%s %s %s %s *" % [
                       info[:datetime][10..11],
                       info[:datetime][8..9],
                       info[:datetime][6..7],
                       info[:datetime][4..5]
                      ]
  # コマンド登録
  config.elements.each_with_index('/project/builders/hudson.tasks.Shell') {|shell, i|
    if i<commands.size
      shell.elements["command"].text = commands[i]
    end
  }

  # 作成
  job_name = "record_%d" % info[:issue_id]
  job = Hudson::Job.get job_name
  job = Hudson::Job.create job_name if job.nil?

  # 設定
  job.update config.to_s
  job.enable
  return true
end

# 予約クエリー検索
reserve = []
queries = Query.find :all, :conditions => { :project_id =>1, :is_public =>1 }
queries.each {|query|

  # REST-APIでクエリー実行
  doc = REXML::Document.new RestClient.get api+query.id.to_s

  # REXMLでトラバース
  doc.elements.each('issues/issue') {|issue|
    if issue.elements['tracker'].attributes['id'].to_i == 1 &&
        issue.elements['status'].attributes['id'].to_i == 1
      # 予約チケットID収集
      reserve << issue.elements['id'].text.to_i
    end
  }
}

Issue.find(reserve).each {|issue|
  channel = CustomValues.first( :conditions => {
                                  :customized_id => issue.id,
                                  :custom_field_id => 2}).value.to_i
  time = CustomValues.first( :conditions => {
                               :customized_id => issue.id,
                               :custom_field_id => 1}).value.to_i

  # 1分前にジョブがスタートするように
  datetime = Time.parse( issue.start_date.strftime("%Y%m%d") + format("%04d",time) ) - 60

  info = {
    # チケット番号
    :issue_id => issue.id,
    # 録画開始日時
    :datetime => datetime.strftime("%Y%m%d%H%M"),
    # 録画時間
    :duration => issue.estimated_hours.to_i * 60,
    # チャンネル
    :channel => channel,
    # 保存場所
    :output => "/opt/videos/%d" % issue.id
  }

  # ジョブ作成
  if create_job info
    # 実行中
    issue.status_id = 2
  end

  # 予約トラッカー
  issue.tracker_id = 2
  issue.save

  # タイムライン更新
  vt = VideoTimelines.where(:issue_id =>issue.id).first
  if vt
    vt.reserved = true
    vt.save
  end

  puts "ID: #{issue.id}"
}
ただし録画開始時間の取り方はTimelineからではなく従来通り。この辺は色々思案中だけど、タイムライン情報は検索と今後のスケジューリング用だと切り分けて考えているんで、一気に変更し過ぎないように進めている。

redmineプラグインフックも更新(video_hooks.rb)
# -*- coding: utf-8 -*-

# 接続設定
Hudson.settings = {:url => 'http://localhost/jenkins', :crumb => false }

# ジョブ作成
def create_job info

  # Config
  return false if !Hudson::Job.list.include?("record_0")
  config = REXML::Document.new(Hudson::Job.get("record_0").config)

  # コマンド定義
  commands = [
              "/opt/task/ready.rb %d" % info[:issue_id],
              "/opt/task/record2.sh %d %d %s" % [
                                                 info[:channel],
                                                 info[:duration],
                                                 info[:output]
                                                ],
              "/opt/task/record2.rb %d" % info[:issue_id]
             ]

  # 時間設定
  config.elements["/project/triggers/hudson.triggers.TimerTrigger/spec"].text =
    "%s %s %s %s *" % [
                       info[:datetime][10..11],
                       info[:datetime][8..9],
                       info[:datetime][6..7],
                       info[:datetime][4..5]
                      ]
  # コマンド登録
  config.elements.each_with_index('/project/builders/hudson.tasks.Shell') {|shell, i|
    if i<commands.size
      shell.elements["command"].text = commands[i]
    end
  }

  # 作成
  job_name = "record_%d" % info[:issue_id]
  job = Hudson::Job.get job_name
  job = Hudson::Job.create job_name if job.nil?

  # 設定
  job.update config.to_s
  job.enable
  return true
end

# ジョブ削除
def delete_job id

  job = Hudson::Job.get "record_#{id}"
  if !job.nil?
    job.delete
    return true
  end
  return false
end

# タイムライン更新
def update_timeline issue
  vt = VideoTimeline.first(:conditions => {:issue_id => issue.id})
  if vt
    vt.reserved = issue.tracker_id == 2
    vt.save
  end
  #多分VideoTimelineクラス内にメソッドとして作るのが正しいと思うが・・・
end

class VideoHooks < Redmine::Hook::ViewListener

  # チケット更新
  def controller_issues_edit_before_save(context)
    edit_hook context[:issue]
  end

  # バルク更新(チケットごとに呼ばれる)
  def controller_issues_bulk_edit_before_save(context)
    edit_hook context[:issue]
  end

  def edit_hook issue
    # 番組内のチケット
    if issue.project_id == 1
      do_cancel issue if issue.tracker_id == 1 || issue.status_id == 5
      do_reserve issue if issue.tracker_id == 2
    end
  end

  # キャンセル
  def do_cancel issue
    # ジョブ削除して、録画キャンセルチケットに
    if delete_job issue.id
      issue.status_id = 5
      issue.tracker_id = 1
      update_timeline issue
    end
  end

  # 予約?
  def do_reserve issue

    time = issue.custom_field_values[0].to_s
    channel = issue.custom_field_values[1].to_s

    # 1分前にジョブがスタートするように
    datetime = Time.parse( issue.start_date.strftime("%Y%m%d") + format("%04d",time) ) + 60
    duration = issue.estimated_hours.to_i * 60

    # 時間調整
    nowtime = Time.now
    if datetime < nowtime
      if datetime + duration < nowtime
        # 終わってる・・・
        issue.tracker_id = 1
        return
      else
        # 時間が過ぎてる!後半だけでも!
        duration = duration - (nowtime - datetime) - 60
        datetime = nowtime + 60
      end
    end

    info = {
      # チケット番号
      :issue_id => issue.id,
      # 録画開始日時
      :datetime => datetime.strftime("%Y%m%d%H%M"),
      # 録画時間
      :duration => duration,
      # チャンネル
      :channel => channel.to_i,
      # 保存場所
      :output => "/opt/videos/%d" % issue.id
    }

    # ジョブ登録してチケットを実行中に更新
    if create_job info
      issue.status_id = 2
      update_timeline issue
    end
  end
end
こっちもタイムラインに関しては、予約情報(reserved)の更新だけ。この情報はまだ未使用。今後のお楽しみかな。

これで一応番組チケット更新処理と、今後のスケジューリングのための用意が整ったかな?

0 件のコメント:

コメントを投稿