2012/07/15

番組データ取得をjenkinsで自動化

録画したTSファイルからEPGデータを取得してredmineチケットにする事は出来るようになった。今度はそれを自動化する必要がある。必要はないか。でも毎日お手手で実行とか馬鹿げてるしね。機械的なルーチンワークはシステム化するのが普通だよね。

Jenkins
CIツールであるjenkinsを使って自動化する事にした。
Jenkinsは以前はHudsonと呼ばれていた、Webベースのビルド自動処理サーバーだ。
通常アプリ開発の効率化とかで便利なシステムだが、今回は番組録画システムの一部として使用してみる。


インストール
https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins+on+Ubuntu
に色々書いてありますが、
$ sudo apt-get install jenkins
だけで済んだよ。ずいぶん前は結構苦労したような・・・

セットアップ
ドキュメントルートはredmineになっているんで、http://localhost/jenkinsでアクセスできるように若干の設定が必要だ。

/etc/default/jenkins ファイルの最後のJENKINS_ARGSに"--prefix=/jenkins"を追加
JENKINS_ARGS="~~ --prefix=/jenkins" って感じに。

/etc/apache/sites-enables/redmineのVirtualHost内に以下を追加
# Jekins
ProxyRequests Off
ProxyPreserveHost on

Order deny,allow
Allow from all
ProxyPass http://localhost:8080/jenkins
ProxyPassReverse http://localhost:8080/jenkins

リスタート
$ sudo service jenkins restart
$ sudo service apache2 reload

これでおしまい。

jenkinsユーザをvideoグループに追加
jenkinsをインストールするとjenkinsユーザが作成され、ジョブはjenkinsユーザで実行される。
使用している録画ツール'recfsusb2n'を実行するにはvideoグループに属していなければならないので、jenkinsユーザをvideoグループに追加した。

実行ユーザを自分の都合のいいユーザに変更する事は出来るが、可能な限り標準状態を崩さないようにしている。
カスタマイズや設定変更が多くなると、保守性が乏しくなるしね。

recepg.sh [channel] 60秒録画したTSファイルからEPGをXMLにするスクリプト
#!/bin/sh
recfsusb2n -b $1 60 $1.ts
epgdump $1 $1.ts $1.xml

entry.rb [channel] xmlでの番組情報からredmineへ番組チケット登録するスクリプト
#!/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 IssueCategories < ActiveRecord::Base
end

class CustomValues < ActiveRecord::Base
end

class CustomFields < ActiveRecord::Base
  # 使いたいな
end

$entry_count = 0
$ch_name = ''

#
# チケット登録
#
def entry_issue date, start, duration, title, desc, category, channel
  #  p date, start, duration, title, desc, category, channel
  #  return

  # カテゴリ
  cate = IssueCategories.first( :conditions =>
                                {:project_id => 1, :name => category } )
  cate = IssueCategories.create( :project_id => 1, :name => category ) if cate.nil?

  # マッチするチケットを検索
  if Issue.count( :conditions => { :start_date => date, :subject => title }) > 0
    return #登録済み
  end

  # チケット作成
  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 => title,
                        :description => desc,
                        :start_date => date,
                        :due_date => date,
                        :estimated_hours => duration,
                        :category_id => cate.id )

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

  if issue.save
    # 開始時間
    CustomValues.create( :customized_type => "Issue",
                         :customized_id => issue.id,
                         :custom_field_id => 1, #'開始時間'
                         :value => start )
    # チャンネル
    CustomValues.create( :customized_type => "Issue",
                         :customized_id => issue.id,
                         :custom_field_id => 2, #'チャンネル'
                         :value => channel )
  end

  $entry_count = $entry_count + 1

end # entry_issue

#
# TVプログラム登録
#
def entry_program prog
  start = Time.parse( prog['start'] )
  stop = Time.parse( prog['stop'] )
  duration = (stop - start) / 60

  date = start.to_date
  start = start.strftime("%H%M").to_i

#  channel = prog['channel'].gsub(/ontv.*/,'') + $ch_name
  channel = prog['channel']+ '.'+$ch_name
  category = prog['category'].join(".")
  title = prog['title']
  desc = prog['desc']
  desc = "" if desc.kind_of? Hash

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

  # デスクリプションが無い番組は登録しない
  return if desc == ''

  title.gsub!(/(【二】|【デ】|【S】|【字】)/,"")

  entry_issue date, start, duration, title, desc, category, channel
end

#
# メイン
#
xml_file = ARGV.shift + '.xml'
h = Hash.from_xml( open(xml_file).read )["tv"]

h.each_pair {|key,value|
  if key == 'channel'
    $ch_name = value['display_name']
  elsif key == 'programme'
    value.each {|prog| entry_program prog}
  end
}

puts "%d entried" % $entry_count

ここは結構苦労しましたよ。前記事でも載せたけど、流れで同じものを置きました。

基本この2つのスクリプトを使って自動化を行う。
ジョブ1:recepg.sh [ch1]
ジョブ2:recepg.sh [ch2]
 ジョブ3:entry.rb [ch1]
  ジョブ4:recepg.sh [ch3]

  ジョブ5:entry.rb [ch2]
   ・・・
   ジョブN:entry.rb [chN]

最初のジョブ1のrecepgをトリガーにして処理が終わったら次のチャンネルのrecepgとチケット登録のentry.rbの2つを実行するように順に処理が行われるようにした。
recepg.shとentry.rbを全部1つのジョブにしなかったのは、recepgの録画処理中に並列処理でチケット登録を行う方が短時間で終わるから。

こういう同期処理とか平行処理とかをスクリプトプログラムで書かなくてもJenkinsで設定すればプログラムレスで自動化できるところがいいね。

1つのジョブの処理を単純化する事が出来るし、実行状況もブラウザから分かりやすく管理できる。タスクの見える化が簡単。タスクのログも残るので、失敗しているときの状況も把握しやすい。

早速失敗が・・・
egpdump がコアダンプで失敗することがある。
以前書いたrecfsusb2nのffmpeg対応処理で行うダミーループカウントを4回に増やしてリビルドした。
安定したようだ。

WANアドレス登録処理もJenkinsでやる事にした
Jenkinsが使いやすいので、以前記事にもしたWANアドレス管理サーバーへのコミット処理もJenkinsで行う事にした。crontabの方をコメントアウトして実行しないように変更した。crontabからのメールが溜まらなくなったのがうれしい。
gauth.py (jenkins対応)
#!/usr/bin/python
# -*- coding: utf-8 -*-

from subprocess import *
from os import path

mail = '????@gmail.com'
password = '????'
app = '????'

app_url = 'http://'+app+'.appspot.com/'
cookie_file = '.google_myapp_cookie'
if path.expanduser("~")[:5]=='/home':
    cookie_file = path.expanduser("~")+'/'+cookie_file
#print cookie_file

def login():
    try:
        # API認証キーを取得
        print 'Get Auth-Key...\n'
        auth = Popen(['curl','-f','-s',
                      'https://www.google.com/accounts/ClientLogin',
                      '-d','accountType=HOSTED_OR_GOOGLE',
                      '-d','Email='  + mail,
                      '-d','Passwd=' + password,
                      '-d','source=' + app,
                      '-d','service=ah'],
                     stdout=PIPE).communicate()[0]
        # ログインしてクッキー取得
        print 'Login...\n'
        login = Popen(['curl','-c','-',
                       app_url+'_ah/login?auth='+auth[ auth.rindex("Auth=")+5:-1]],
                      stdout=PIPE).communicate()[0]
        cookie = login[login.rindex("ACSID")+6:-1]

        # クッキーの保存
        f = open(cookie_file,'w')
        f.write(cookie)
        f.close()
        return cookie
    except:
        return ""

def get(controller):
    for i in range(2):
        try:
            f = open(cookie_file)
            cookie = f.read()
            f.close()
            ret = Popen(['curl',app_url+controller,'-b','ACSID='+cookie],
                        stdout=PIPE).communicate()[0]
            if ret.find('COMMITED')>0:
                return 0
            else:
                raise
        except:
            cookie = login()
    raise RuntimeError('Login Failure')


jenkinsに失敗が伝わるようにログイン失敗したらpythonも失敗するようにgauth.pyを変更した。
jenkinsから実行された場合cookieファイルがjenkinsのワークスペースに保存されるように変更。

さて、これで日々の番組がredmineチケットに自動登録されるところまでが出来た。
次は録画予約かな。

0 件のコメント:

コメントを投稿