2015/07/28

クエリレコード作成と番組自動予約

手動で予約~録画、視聴の流れはできたかな。次は自動予約だ。
前回はRedmineプラグインだったので制約が結構あったが、今回は自由がある。
その代わり面倒なところもある。

Queryモデル作成

$ rails g  scaffold query title:string start_time:time end_time:time query_type_id:integer category_id:integer sub_category_id:integer priority:integer

Queryレコード作成・変更・削除が必要なのでscaffoldで生成したコードを全部使用する。

query_type_idは、予約:1か検索:2を示す部分で、自動予約処理で使用する。
予約したいわけじゃなく「今日のゴールデンタイムのドラマは?」 を見たいときは検索タイプ。

start_timeとend_timeは、日時ではなく時間帯を指定したいので datetime ではなく time 型である。

priorityは将来的なもので、自動予約で時間がバッティングした場合、どちらを優先するのか?を設定しておくことで勝ち負けも自動でやらせようかという思惑。

models/query.rbの中身
# -*- coding: utf-8 -*-
class Query < ActiveRecord::Base
  belongs_to :category, -> { where cate_type: 0 }
  belongs_to :sub_category, -> { where cate_type: 1 }, class_name: 'Category'
  belongs_to :query_type

  def list
    programs = Program.all
    programs = programs.where(category_id: category_id) if category.present?
    programs = programs.where(category2_id: sub_category_id) if sub_category.present?
    programs = programs.where("title like :name", name: "%#{title}%") if title.present?

    # todo: 時間がセットされていないと判断する賢い方法はないのか?
    unless start_time.strftime("%H%M") + end_time.strftime("%H%M") == '00000000'
      programs = programs.where("TIME(start_at) >= TIME(?) and TIME(start_at) <= TIME(?)",
                                start_time, end_time)
    end
    return programs
  end

end

queryのlistメソッドで条件にマッチした番組リストを返す。条件が設定されているかどうかを値が入っているかどうかで判断したいけど、time型の場合が厄介。

f.time_selectでblankありにして、blank設定のままでも"0:00"になってしまう。nilにならない。

これをどうしたら良いの?という記事はいくつか見つかる。
http://stackoverflow.com/questions/14367705/time-select-blank-field-saves-a-default-time-when-form-is-submitted
とかで語られているソリューションがほとんど。だけど自分の肌に合わなかったので、
start_time.strftime("%H%M") + end_time.strftime("%H%M") == '00000000'
と、若干強引な方法で確認する羽目となった。

検索結果表示は、番組リスト(Program#index)で済ませる
内部処理はこんな↓
    @q = Program.where(video_id: nil).ransack(params[:q])
    if params[:query_id].present?
      @programs = Query.find_by(params[:query_id]).list
    else
      @programs = @q.result
    end
    @programs = @programs.order(:start_at).page(params[:page])
query_id付きで呼ばれた場合と普通の時で処理分け。
ransackも機能する必要があるので、@qの処理はどっちでもやる。

自動予約

繰り返し実行なのでActiveJobではなく、rake task にして whenever で回す。
タスクはすごく単純。 予約タイプのQueryレコードから条件にマッチする番組の予約をするだけ。
# -*- coding: utf-8 -*-
namespace :query do

  desc "自動予約"
  task :reserve => :environment do
    Query.where(query_type_id: 1).each do |q|
      q.list.where(video_id: nil).where('start_at > ?',Time.now+60).each do |program|
        program.reserve
      end
    end
  end

end

予約済みや、今より過去の番組を予約しないように、若干の条件付けを施す程度。
これだけで結構ちゃんと機能している。
以前に書いた処理に比べれば大分シンプルにまとまったような気がする。

これで全体機能は一段落ですが、実際に運用し始めるとまた色々問題が出るんですよねぇ。

2015/07/25

予約レコード周りのその他こまごましたところ

大筋の機能は何とかイメージ通りに実装できたのですが、細かいところでも知らないがゆえに
引っかかりながらの実装となった点をまとめておく。

flashメッセージ表示

予約成功!、予約失敗などを表示するのをflashメッセージを使ってやろうとしたんだけれど、
bootstrapの場合、Rails標準のflashタイプじゃなくてbootstrap後に変換が必要なことを知らず
ちょっとはまってしまった。

通常 redirect_to うにゃうにゃ, notice: "成功!" ってやると思うんだけれど
この"notice"をbootstrap的にはclass "alert alert-success"とかに変換しないと出ない。
その辺りの記事は沢山あった。よくあるパターンとしては
notice => alert-info、alert => alert-warning、error => alert-danger とかに対応させている。
この通りやるのだが、メッセージが表示されない。もしくは無色で表示される。
違う。バックが青とか赤で囲われて欲しいのだよ。結局自分の場合、

notice => alert-success、alert => alert-danger とした。
バックカラーが、successがグリーン、dangerが赤で表示されるから。それ以外は白?だった。
それに、redirect_toのオプションで付けられるのは、notice、alert、flash => {ゴニョゴニョ} の3つ。
noticeとalertだけなら簡単だし。

予約時間の重複チェック

チューナー1つしか無いので、予約しようとした時間帯に引っかかる予約済みレコード確認をして、引っかかっしまう場合はエラーにしたいわけです。
検索としては、既存レコードの開始時間がその時間帯内である もしくは 終了時間がその時間帯内にある場合は・・・です。ということは ”OR” でつなげるわけです。
Program.where(ゴニョゴニョ)で一気に書こうとするとSQL文をなが~く書かないとならない。
嫌なので、"OR"はRubyでやることで全体コードをシンプルにした。

video_on = Program.where.not(video_id: nil)
start_on = video_on.where(start_at: (@program.start_at..@program.end_at))
end_on = video_on.where(end_at: (@program.start_at..@program.end_at))
if start_on.present? or end_on.present?
  redirect_to program_url, alert: 'Reserved violation another videos'
return
end
こんな感じ。BETWEEN生成はwhereに任せ、面倒なORはRubyで。
連続しているものは許したいので実際にはstat,endは微妙に調整する。

録画ジョブ開始タイミングは番組時間の30秒前

ActiveJobのperformが開始されて実際の録画を開始するまでには結構時間がかかる。
なのでJenkins処理でやっていた時のように、少し早めにスタートさせて sleep でタイミング調整
をするようにしている。
recfsusb2n自体にも--waitで待つ機能があるようだけど正確に働かない感じだったので
スクリプト内のsleepで待つようにした。

録画時間は15秒前終了

重なってはいないけど録画時間が連続している場合、多少のギャップを作ってチューナー
を空けておかないと録画開始時にデバイスエラーになってしまうので、若干短めにしておく。


とまあ、細かいところだけれど、録画失敗してがっかりとならないようにやっておかないとね。
で実際の録画ジョブコードは以下のとおり。
  def perform(video)
    if video
      # write path
      path = video.program.start_at.strftime("%Y%m")
      fdir = "#{VIDEO_ROOT}/#{path}"
      Dir.mkdir(fdir, 0777) unless Dir.exist?(fdir)

      fname = "#{path}/#{video.id}"
      fpath = "#{VIDEO_ROOT}/#{fname}"

      # recording
      channel = video.program.channel.ch
      duration = video.program.duration - 15
      wait = (video.program.start_at - Time.now).to_i

      sleep wait if wait>0

      p "------recording start: #{fname}"
      begin
        `tvoff`
        # recode
        `recfsusb2n -b -i hd #{channel} #{duration} #{fpath}.ts`
        `tvon`
        # encode
        p `ffmpeg -loglevel 8 -y -i #{fpath}.ts -c:v libx264 -c:a libfaac -preset superfast -f\
 mp4 -threads 2 -b:v 2000k #{fpath}.mp4`
        # poster
        p `ffmpeg -loglevel 8 -i #{fpath}.mp4 -ss 12 -vframes 1 -an -s 320x180 #{fpath}.jpg`
        # successed
        video.update_attributes(status_id: 2, filename: "#{fname}") # successfully.
        File.delete("#{fpath}.ts")

      rescue => e
        p e.message
        video.update_attributes(status_id: 3) # recording job failed.
      end
      p "------recording end: #{fpath}"
    end
  end

録画の前後でtvon、tvoffっていうのを実行するようになっていますが、これは自分が適当に
作ったシェルスクリプトでして、以下の様なシロモノです。
tvon
#!/bin/bash
recfsusb2n -b -i hd -H 8888 1>/dev/null 2>/dev/null
tvoff
#!/bin/bash
PID=`pidof recfsusb2n`
if test "$PID" != "" ; then
    kill -9 $PID
fi
録画していない時はリアルタイム視聴できる状態にスタンバイさせておこうというものです。
朝方とか結構重宝します。家族食事中は子供にTVを見せないポリシーなもんで。

2015/07/23

録画予約をActiveJob with sidekiq で

前回の記事で、録画ジョブをActiveJobでやる話を書きました。
実際にやってみて、これまた色々ハマったので今回はそのへんの話を書きたいと思います。

要件
1.EPG情報から直近7日間中の何時何分に実行という予約ができること。
その間、システムリブートしても予約が消滅しないこと。キューの永続化。
2.ジョブが失敗してもリトライしない。スケジュールした時間に意味あるので。
4.予約したものを途中キャンセルできること。


RecordJob という ActiveJob::Baseの派生クラスを作成
$ rails g job record
すると、app/jobs/record_job.rbファイルが作られます。最初は以下の様な感じ。
class RecordJob < ActiveJob::Base
  queue_as :default

  def perform(*args)
    # Do something later
  end
end
このperform関数内に録画処理をずらずらと書いておくわけですが、その内容とは関係ない周辺処理でいくつかハマってしまいました。

1.指定日時分に実行するジョブ登録と実行(perform)

登録自体は簡単です。
  # 予約
  def reserve
    start = self.program.start_at - 30
    job = RecordJob.set(wait_until: start).perform_later(self)
    update_attribute(:job_id, job.job_id)
  end
このなメソッドを、ビデオレコードモデル models/video.rb に書いておいて、予約時に video.reserveを呼ぶだけです。
RecordJob.set(wait_until: start).perform_later(self) がActiveJobを通してSidekiqにstartで指定した日時分に実行するスケジュールが登録されます。redisにスケジュールは永続化するので、途中システムリブートしても大丈夫。

なぜ perform_later(video.id)ではなく、perform_later(self) なのか?

ネット記事を読みながらやると、オブジェクト渡しはシリアライズが入ってよろしくないからID渡しをするのだ的な内容が多かったので、最初は自分もそうしてみました。
 ところが!
最初のperform実行ではちゃんとvideo.idが渡って滞り無く処理されるんですが、不思議なことに2回めのジョブ(もちろんIDは異なる)でも最初のIDでperformが実行されてしまいました。
perform(video_id)で受けるのをやめてみて perform(*args) で受けてみても結果同じでした。

↑後で気づいたんだけど、whereではなくfind_byを使ったためと思われる。perform(id)ができないというわけではないようだ。

その後知りましたが、現在のActiveJobのperformへはオブジェクト渡しは悪い方法ではないらしい。シリアライズ負荷がどうのという懸念は解消しているらしい。

ただオブジェクト渡しだと、ジョブ実行前にそのオブジェクトレコードが削除された場合は事前に調べることが出来ず、例外エラーとなってしまう。これはちゃんとレコード削除とジョブキャンセルを同期させれば問題ないか。

2.失敗してもリトライしないように

ネット上は色々なソリューションがあるようです。

ActiveJobオプションをジョブクラスに記述する。
job_options retry: false

Sidekiqオプションを記述する。
sidekiq_options :retry => false

ふむふむ? どっちも効きませんけど・・・
結局自分は、以下のコードを、initializers/sidekiq.rb に加えました。
Sidekiq.configure_server do |config|
   config.server_middleware do |chain|
     chain.add Sidekiq::Middleware::Server::RetryJobs, :max_retries => 0
   end
end

3.SidekiqスケジュールをRailsアプリからキャンセルする

放っておいてもジョブ失敗するだけでシステムが死ぬわけじゃないんで支障はない
のだけれど、ログが汚くなるし、sidekiq_webで見た時にわけわからなくなるんで、
予約キャンセルでジョブスケジュールも削除したいわけです。

探してみましたが、ActiveJobからキュー状態を調べたり削除したりすることが出来ない?

perform_laterが返すJobのjob_idは、SidekiqのjobIDとは異なる? むむぅ。調べた結果、
Sidekiqスケジュールのアーギュメント内にActiveJobのjob_idが記録されている。
ということで以下のコードをレコード削除メッソド内に記述することで解決。
jobs = Sidekiq::ScheduledSet.new.select do |job|
  job.args[0]['job_id'] == @video.job_id
end
jobs.each(&:delete)
冒頭のreserveメソッドでActiveJobのjob_idをレコード上に覚えておくのは、このため。
以上が、やりたいこと以外のやらなくてはならなかったこと。結構苦労した。

2015/07/19

録画処理をActiveJobで

実際の録画ジョブをどこでどうやって動かすか?

過去のアプローチとしては、Linuxの'at'コマンドでOS的なジョブ登録で回していた。
'at'は、お手軽でいいんだけれど、ジョブ管理が結構面倒だった。

その後、Jenkinsでジョブ管理するようにした。これはこれで良かったが、Jenkins自体がメモリとCPU食いなので、1台で全部やらせようとするとキャパ的にきつかった。

今回完全リニューアルということで、調べなおしてみたところ、Rails4以降では、ActiveJob というものが標準化したらしい。ならば、使ってみようじゃないですかと。

ActiveJobはただのフロントAPIであって、実際のジョブは別のジョブキューイングシステムに任せるようだ。その辺の解説は、http://blog.chopschips.net/blog/2015/02/26/active-job/がいいです。

Sucker Punch

バックエンドに簡単そうな、Sucker Punch を使ってやってみることにした。作者の記事もなかなか面白いです → Why I Wrote the Sucker Punch Gem

いつものようにGemfileに、gem 'sucker_punch' を追記して、$ bundle しておく。
config/initializers/sucker_punch.rbファイル作成して
Rails.application.configure do
  config.active_job.queue_adapter = :sucker_punch
end
と書いて、ActiveJobのバックエンドとしてSuckerPunchを使うようにして準備完了。
簡単にできるはずが・・・

実は色々ハマりました
SuckerPunchは実は Celluloid のラッパーに等しい。このCelluloidの挙動がむずい。

1.予約録画モデルのVideoモデルがジョブキューを持てばいいんじゃないか?
と、いきなりSuckerPunchを直接使ってしまおうと思った。そこで、Videoモデルに
include SuckerPunch::Job
を入れてみたら、Videoモデル全体がCelluloidでラップされてしまい、.createとか、.first_or_createとかが全然動かなくなってしまった。newしてsaveすれば?と書き方を変えて粘ってもみたが、 全く太刀打ち出来無かった・・・

気持ちを新たに、rails g job RecordJob で、RecordJobというActiveJobを作ってからのこと・・・

2.perform_laterと10秒制限?
RecordJob.perform_laterでキュー登録してみた。performメソッド内で録画タイミングを図るためにsleepをさせていた。perform_laterからperformはコールされるが、perform内は10秒以内の処理じゃないと強制的にアボートしてしまうようだ。はぁ?って感じ。

 RecordJob.set(wait_until: video.program.start_at).perform_later(video)
気持ちとしては、上記の1行で予約時にキュー登録したいのだが、試しにやってみたら、wait_untilはSuckerPuchでは実装されていなかった・・・

残念ながら、SuckerPunchは目的には合致しなかったようだ。撃沈。

やっぱり普通にSidekiqか

sidekiqをバックエンドに使用することにしたので redis 入れる。


$ apt-get install redis-server

Gemfileにgem 'sidekiq'を追加して、$ bundle実行。
sidekiqは、$ sidekiq で直接動かしてテスト実行。

今度は、
 RecordJob.set(wait_until: 録画開始時間).perform_later(video)
が普通に動いた。
後は、sidekiqをサーバ自動起動化して、タスク環境はよしとしよう。

lm-sensors 入れておく

Ubuntu14.04 on DS57Uに色々構築中だが、ファンレスPCは自分自身初めてなので、これからの猛暑を乗りきれるのか?と不安で。

で、そういえば以前も確認したりしてたなぁと思い出し、lm-sensors入れておこうと思います。

$ sudo apt-get install lm-sensors
$ sudo sensors-detect

の時の表示ログ↓
# sensors-detect revision 6170 (2013-05-20 21:25:22 +0200)
# System: Shuttle Inc. DS57U [V1.0]
# Board: Shuttle Inc. FS57U

This program will help you determine which kernel modules you need
to load to use lm_sensors most effectively. It is generally safe
and recommended to accept the default answers to all questions,
unless you know what you're doing.

Some south bridges, CPUs or memory controllers contain embedded sensors.
Do you want to scan for them? This is totally safe. (YES/no):
Module cpuid loaded successfully.
Silicon Integrated Systems SIS5595...                       No
VIA VT82C686 Integrated Sensors...                          No
VIA VT8231 Integrated Sensors...                            No
AMD K8 thermal sensors...                                   No
AMD Family 10h thermal sensors...                           No
AMD Family 11h thermal sensors...                           No
AMD Family 12h and 14h thermal sensors...                   No
AMD Family 15h thermal sensors...                           No
AMD Family 15h power sensors...                             No
AMD Family 16h power sensors...                             No
Intel digital thermal sensor...                             Success!
    (driver `coretemp')
Intel AMB FB-DIMM thermal sensor...                         No
VIA C7 thermal sensor...                                    No
VIA Nano thermal sensor...                                  No

Some Super I/O chips contain embedded sensors. We have to write to
standard I/O ports to probe them. This is usually safe.
Do you want to scan for Super I/O sensors? (YES/no):
Probing for Super-I/O at 0x2e/0x2f
Trying family `National Semiconductor/ITE'...               No
Trying family `SMSC'...                                     No
Trying family `VIA/Winbond/Nuvoton/Fintek'...               No
Trying family `ITE'...                                      Yes
Found `ITE IT8728F Super IO Sensors'                        Success!
    (address 0xa40, driver `it87')
Probing for Super-I/O at 0x4e/0x4f
Trying family `National Semiconductor/ITE'...               No
Trying family `SMSC'...                                     No
Trying family `VIA/Winbond/Nuvoton/Fintek'...               No
Trying family `ITE'...                                      No

Some systems (mainly servers) implement IPMI, a set of common interfaces
through which system health data may be retrieved, amongst other things.
We first try to get the information from SMBIOS. If we don't find it
there, we have to read from arbitrary I/O ports to probe for such
interfaces. This is normally safe. Do you want to scan for IPMI
interfaces? (YES/no):
Probing for `IPMI BMC KCS' at 0xca0...                      No
Probing for `IPMI BMC SMIC' at 0xca8...                     No

Some hardware monitoring chips are accessible through the ISA I/O ports.
We have to write to arbitrary I/O ports to probe them. This is usually
safe though. Yes, you do have ISA I/O ports even if you do not have any
ISA slots! Do you want to scan the ISA I/O ports? (yes/NO):

Lastly, we can probe the I2C/SMBus adapters for connected hardware
monitoring devices. This is the most risky part, and while it works
reasonably well on most systems, it has been reported to cause trouble
on some systems.
Do you want to probe the I2C/SMBus adapters now? (YES/no):
Found unknown SMBus adapter 8086:9ca2 at 0000:00:1f.3.
Sorry, no supported PCI bus adapters found.
Module i2c-dev loaded successfully.

Next adapter: i915 gmbus ssc (i2c-0)
Do you want to scan it? (yes/NO/selectively):

Next adapter: i915 gmbus vga (i2c-1)
Do you want to scan it? (yes/NO/selectively):

Next adapter: i915 gmbus panel (i2c-2)
Do you want to scan it? (yes/NO/selectively):

Next adapter: i915 gmbus dpc (i2c-3)
Do you want to scan it? (yes/NO/selectively):

Next adapter: i915 gmbus dpb (i2c-4)
Do you want to scan it? (yes/NO/selectively):

Next adapter: i915 gmbus dpd (i2c-5)
Do you want to scan it? (yes/NO/selectively):

Next adapter: DPDDC-B (i2c-6)
Do you want to scan it? (yes/NO/selectively):

Now follows a summary of the probes I have just done.
Just press ENTER to continue:

Driver `it87':
  * ISA bus, address 0xa40
    Chip `ITE IT8728F Super IO Sensors' (confidence: 9)

Driver `coretemp':
  * Chip `Intel digital thermal sensor' (confidence: 9)

To load everything that is needed, add this to /etc/modules:
#----cut here----
# Chip drivers
coretemp
it87
#----cut here----
If you have some drivers built into your kernel, the list above will
contain too many modules. Skip the appropriate ones!

Do you want to add these lines automatically to /etc/modules? (yes/NO)yes
Successful!

Monitoring programs won't work until the needed modules are
loaded. You may want to run 'service kmod start'
to load them.

Unloading i2c-dev... OK
Unloading cpuid... OK



取得可能な情報は、coretemp(CPU温度)とSuper IOでした。Super I/Oとは、
https://en.wikipedia.org/wiki/Super_I/O によると低速な外部デバイスポートを接続するコントローラの部分でしょうか。とりあえず欲しいのはCPU温度だけなので、coretempさえ取れればいいや。
因みに、/etc/modulesにセンサードライバを記述するのだが、それもsensors-detectさんにやってもらいました。

$ sensors

正午ごろの結果↓
acpitz-virtual-0
Adapter: Virtual device
temp1:        +27.8°C  (crit = +105.0°C)
temp2:        +29.8°C  (crit = +105.0°C)

coretemp-isa-0000
Adapter: ISA adapter
Physical id 0:  +44.0°C  (high = +105.0°C, crit = +105.0°C)
Core 0:         +42.0°C  (high = +105.0°C, crit = +105.0°C)
Core 1:         +43.0°C  (high = +105.0°C, crit = +105.0°C)


ふむふむ。思ったほど高温でもないような。



CPUの動作周波数は?

$ grep MHz /proc/cpuinfo
 cpu MHz         : 806.718
 cpu MHz         : 799.746
DS57UのCPUは、Celeron 3205U(1.5GHz) なので、こんなもんでしょうねぇ。


無駄記事だったかもしれないけど、これから猛暑に突入なのでファンレスPCの温度管理は時々しておいたほうがいいし、最近この手の記事が見当たらないので補填の意味で。

video(録画)テーブルの設計と実装

番組テーブルの実装とリスト表示、検索、ソート等の実装がおおかた落ち着いたので、次はいよいよ予約、録画、視聴のためのレコード設計に入ります。

予約・録画レコードを、Videosとするとして、それがどのような状態かを表す必要がありますね。
予約・録画・失敗とかです。
それを表現するためのマスターレコードをまずは作っておきましょう。

ステータスレコード
$ rails g model status name:string
で、ちょっと乱暴ですが、レコードID決め打ちで意味をもたせます。単純にこれだけなので、seedで入れてしまいます。
db/seeds.rb
 Status.create(name: '予約')
 Status.create(name: '録画')
 Status.create(name: 'キャンセル')
 Status.create(name: '失敗')

$ rake db:migrate
$ rake db:seed
これでステータスレコードは完成。

ビデオレコード
$ rails g scaffold video status_id:integer program_id:integer filename:string
後で全部書き換えることになると思うけれど、scaffoldで初期状態のviews/controllersを作ってもらいます。
それに、後で幾つかレコードデータも追加していくことになるでしょう。

status_idが、先ほどのStatusへのリンク用
program_idが、番組テール部へのリンク用
filenameが、録画したファイル名。ファイルパスとか何とかは後で考える。

論理削除を使うかやめるか?
Programレコードの方だが、過去レコードはどんどん削除するつもりでいるが、Videoレコードには番組情報を持たずに、program_idでリンクを貼るつもりでいる。ならば過去レコードであっても削除されては困るわけで。
じゃあ、deleted_atを付けて、「paranoia」とかで論理削除を組み込むか?組み込むなら、
Gemfileに
 gem "paranoia", "~> 2.0"
を追加(rails 4.xは、v2.xを使うらしい)して、models/programs.rbに
 acts_as_paranoid
と書けば組み込まれるので簡単ではあるが・・・

やっぱりやめて、video_idをProgramベーブルに追加する方向で。
$ rails g AddVideoIdToProgram video_id:integer:index ←面倒になってきたのでindexも一緒に。

このvideo_idがNULLじゃなければ消さないようにする。今回の場合はこの方がシンプルで分かりやすいかなと。

確認
$ rails s -b 0.0.0.0
http://shuttle:3000/videos へアクセス。
おや? そうでした、まだ、bootstrapテーマをかぶせてませんでした。
$ rails g bootstrap:themed Videos
はい。いい感じ。
これにProgramsと同じように検索とソートを加えて見た目実装は完了。

次は実際の「予約・録画」ですか。ここまでほとんど自分でコードをガリガリ書くこと無く出来てしまいましたが、さすがにここからはコードを書かないとならんですねぇ。

ransackで検索とソートを組み込み

ransackを使って、番組リストの検索とソートをさくっと組み込みます。

Gemfileにgem 'ransack'書いて、bundle install。準備完了。

app/controllers/programs.rbのindex部は

に書き換え。searchとresultがransackの処理。
# @programs = Program.page(params[:page]) を下の2行に書き換え。
@q = Program.search(params[:q])
@programs = @q.result.page(params[:page])
controllerはこれだけで終わり。

ソートはテーブルヘッダをsort_linkで書き換える。
      <th><%= sort_link(@q, :channel_name, model_class.human_attribute_name(:channel_id)) %></th>
な風に。

views/programs/index.html.erb内の検索フォーム:

      <%= search_form_for @q do |f| %>
      

<%# include_blank:  でブランク行含む。文字列にすることも出来る%>

 <%= f.collection_select :channel_name_cont, Channel.all, :name, :name, include_blank: model_c\
lass.human_attribute_name(:channel_id) %>

 <%#= f.collection_select :channel_id_cont, Channel.all, :id, :name, include_blank: true %>

      
<%# multiple: true で複数選択 %>
<%= f.collection_select :category_name_cont, Category.where(cate_type:0).order(:name), :name,\
 :name, include_blank: model_class.human_attribute_name(:category_id), multiple: true %>
<%# multiple: true で複数選択 %>
<%= f.collection_select :category2_name_cont, Category.where(cate_type:1).order(:name), :name\
, :name, include_blank: model_class.human_attribute_name(:category2_id), multiple: true %>
<%# _gtでそれ以上という検索になる %>
<%= f.search_field :duration_gt, size: 5, placeholder: model_class.human_attribute_name(:dura\
tion) %>
<%= f.search_field :start_at_date_equals, size: 10, placeholder: model_class.human_attribute_\
name(:start_at) %>
<%= f.search_field :end_at_date_equals, size: 10, placeholder: model_class.human_attribute_na\
me(:end_at) %>
<%= f.search_field :title_cont, placeholder: model_class.human_attribute_name(:title) %>
<%= f.submit %>
<% end %>
channelは、collection_selectを使ってプルダウンメニュー型に
categoryは、Categoryレコード内に2タイプのカテゴリを混ぜているので、allではなくwhereでフィルタしています。
duration_gtですが、_gtを付けると「以上」を表現できます。
start_at_date_equalsの_date_equalsは、入力した文字列をDateに変換させる拡張をしています。

_date_equals拡張:
models/programs.rbに以下を加える

  ransacker :start_at do
    Arel.sql('date(start_at)')
  end
  ransacker :end_at do
    Arel.sql('date(end_at)')
  end
config/initializers/ransack.rbファイルを作成して以下のとおりとする

Ransack.configure do |config|
  # search_fieldで_date_equalsの内部処理                                                              
  config.add_predicate 'date_equals',
  arel_predicate: 'eq',
  formatter: proc {|v|
    begin
      v.to_date
    rescue
    end
    },
  validator: proc {|v| v.present? },
  type: :string
end
詳しくはこちら、
https://github.com/activerecord-hackery/ransack/wiki/Using-Ransackers
に書いてあるとおりです。

turbolinks問題
えーと、実はよくわかってはいないのですが、Rails4では、turbolinksとやらがデフォルトでつくようになったそうで、これが色んな所でよくわからない挙動というか挙動しない現象がありました。

ransackのsort_linkでソートした後にsearch_form_forのsubmitが効かなくなる!
link_to で、programs_path へ飛ばした後のsearch_form_forのsubmitが効かなくなる!
いろいろ調べて、jquery-turbolinksを入れてみましたが結果変わらず。
link_toに "data: {no_turbolink: 1}"を付けてみたところ機能はしましたが、sort_linkにこのオプションを付ける方法が分からず断念。

結局、turbolinksを外すことで解決となりました。

ルック&フィールを一気に片付ける

kaminari でページ、bootstrapで全体フィール、ransackで検索とソート を作っておしまいにする

bootstrapとkaminariは以下の記事を参考にして、さくさくっと。
http://ruby-rails.hatenadiary.com/entry/20140801/1406818800
http://ruby-rails.hatenadiary.com/entry/20141113/1415874683#kaminari-install-with-bootstrap

Gemfileに以下を追加して、bundle install !
gem 'kaminari'
gem 'less-rails'
gem 'twitter-bootstrap-rails'
gem 'ransack'

kaminari
$ rails g kaminari:config

コントローラファイル編集
@programs = Program.all

@programs = Program.page(params[:page])

ビューファイル編集
<%= paginate @programs %>

を最後に加えるだけ

bootstrap
$ rails g bootstrap:install
$ rails g bootstrap:themed Programs
$ rails g kaminari:views bootstrap3 ← rail3だと適応できなかったけど、rail4だと大丈夫だ。

これでProgramビューを一気にいい感じにしてくれる。


ここまでやったら、viewsを少し整えます。

models/programs.rb

  belongs_to :channel
  belongs_to :category
と書いておくと、あら不思議、channel_idとcategory_idから勝手にjoinした状態でアクセスできるようになります。

views/programs/index.html.erb
@program.channel_idを、@program.channel.nameに書き換えれば、その番組のchannel名を簡単に表示される。

scaffoldやbootstrapで自動作成したリストから不要な表示を削除してすっきりさせる。
showページへは、idからのリンクではなく、titleからのリンクにしたいので
id表示を削除して、title部分を

<td><%= link_to program.title, program_path(program) %></td>

のようにします。
detailや、録画予約ボタンはshowページで後ほど。

config/initializers/time_formats.rb ファイルを作成し、以下のように

Time::DATE_FORMATS[:default] = '%Y/%m/%d %H:%M'
class Integer
    def time_duration
        Time.at(self).utc.strftime('%H:%M')
    end
end

開始と終了時間、及び長さの時間表示フォーマットをここで定義しちゃいます。
durationは整数なんでIntegerクラスに変換メソッドを定義しておきます。

で、start_at,end_atは勝手にそのフォーマットで表示され、durationは、duration.time_durationってやれば時:分で表示されるように。

後は、config/application.rb で日本だよってやって、
    config.time_zone = 'Tokyo'
    config.active_record.default_timezone = :local
    config.i18n.default_locale = :ja

config/locales/ja.yml を用意して
ja:
  activerecord:
    models:
      program: 番組
    attributes:
      program:
        title: タイトル
      programs:
        title: タイトル
  title: タイトル
  start_at: 開始時間
  end_at: 終了時間
  duration: 長さ
  channel: チャンネル
  category: カテゴリ
 みたいにしておくとリストヘッダとか諸々日本語になる。
 プライベートアプリなので、直接日本語書いてしまえばいいと思うが、遠回りすることが実験なのでね。

2015/07/11

epgのデータベース登録

シンプルなサーバーアプリが出来たところで番組データを取り込んで、リストアップするところまで。

以前は素のRubyスクリプトでActiveRecordを直接使う流れでしたが、今回はRailsのタスクで用意してみる。

lib/tasks/epg.rakeファイルを作成。以下の様な感じになった。

タスクからタスクを呼ぶのは、executeとinvokeがあるが、invokeは処理中に1度しか呼ばないという仕様がある。チャンネル分回すので、今回はexecuteにした。

タスク引数は、Rake::TaskArgumentsを使うことになるが、executeの場合シンプルに渡すだけらしいので、execute(ch)ってやれば、|task,arg|のargに直接chが入るので、スッキリはする。
ただし、executeからしか呼べなくなるという制約がつく。外から引数付きのタスクとして呼べなくなる。

それから、:updateはただ呼ぶだけのタスクだから :environment は不要だろうと思ったが、親タスクも必要だった。因みに、 :environment はActiveRecordでのレコードクラスインスタンス作成などをやってくれるところらしい。詳しくは知らないっすが・・

後は、んん?というところは無いはず。ごく自然な流れ。


namespace :epg do

  EPG_PATH = Rails.root.join('tmp').join('epg')
  Chs = [27,26,25,24,23,22,21,16]
#  Chs = [27]

  desc "EPG 更新"
  task :update => :environment do
    Chs.each do |ch|
      arg = Rake::TaskArguments.new([:ch],[ch])
      Rake::Task['epg:get'].execute(arg)
      Rake::Task['epg:put'].execute(arg)
#      Rake::Task['epg:get'].execute(ch) ##1 個別に呼ぶのを諦めれば、こっちでもいける.
    end
  end

  desc "EPG 取得"
#  task :get do |t,arg| ##1
  task :get,[:ch] do |t,arg|
    `recfsusb2n -b -i epg #{arg.ch} 10 #{EPG_PATH}/#{arg.ch}.ts`
    `epgdump json #{EPG_PATH}/#{arg.ch}.ts #{EPG_PATH}/#{arg.ch}.json`
  end

  desc "EPG 登録"
  task :put,[:ch] => :environment do |t,arg|
    json = open("#{EPG_PATH}/#{arg.ch}.json").read
    data = ActiveSupport::JSON.decode(json)

    if data.present?
      # Channelレコード 登録
      Channel.create( ch: arg.ch,
                      name: data[0]['name'],
                      code: data[0]['id']) if Channel.where(ch: arg.ch).empty?

      # 過去のプログラムを削除
      Program.destroy_all ["end_at < ?",Time.now]

      # 登録
      data[0]['programs'].each do |prog|
        set_program( prog )
      end
    end
  end

  # レコード 登録
  def set_program( prog )
    if Program.where(event_id: prog['event_id']).empty?

      # category
      category = prog['category']
      if category.present?
        cate = Category.where(name:
                              [category[0]['large']['ja_JP'],' (',category[0]['middle']['ja_JP\
'],') '].join
                              ).first_or_create
      end

      # program
      Program.create( channel_id: Channel.where(code: prog['channel']).first.id,
                      event_id: prog['event_id'],
                      title: prog['title'],
                      detail: prog['detail'],
                      start_at: Time.at(prog['start']/10000),
                      end_at: Time.at(prog['end']/10000),
                      duration: prog['duration'],
                      category_id: cate.try(:id) )
    end
  end
end

2015/07/10

録画システムをrails4で開発

録画ツールや開発環境が整いましたので、いよいよ録画システムのフロントエンドとなる
サーバーアプリケーションをRails4を使って構築していきます。

mysql

用途から言ってsqlliteで十分だと思うけれど、データベースは使い慣れているmysqlにする。

$ sudo apt-get install mysql-server
$ sudo apt-get install libmysqlclient-dev ← gem mysqlで使うから

後これくらいはやっておこう。
$ mysql_secure_installation

ユーザ作成
mysqlにrecmanユーザを作成して、recmanデータベースのアクセス権を与える。

$ mysql -uroot -p
> create user 'recman'@'localhost' identified by 'pw';
> grant all privileges on recman.* to 'recman'@'localhost';


railsアプリ(recman)の作成
$ rails new recman -d mysql

gitリポジトリ
$ cd recman
$ git init
$ git add .
$ git commit -m "the first recman"

gem
recman/conf/database.ymlのproductionセクションが以下のようになっていたので
production:
  <<: br="" default="">  database: recman_production
  username: recman
  password: <%= ENV['RECMAN_DATABASE_PASSWORD'] %>
 ↑直接パスワードに書き換える。環境変数で管理するほどじゃないし。
その他データベース名とかは自分のすきに書き換える。

Gemfileに追記
gem 'therubyracer'
gem 'mysql'
gem 'unicorn' ←productionをunicornで駆動するから


今回デプロイしないで直接駆動の予定なので、capistranoは使わないな。

$ bundle install

これでrailsアプリの母体が出来ました。

model作成

Channelモデル
$ rails g model channel code:integer name:string

Categoryモデル
$ rails g model category name:string

Programモデル
$ rails g scaffold program \
    channel_id:integer \
    title:string \
    detail:text \
    start_time:datetime \
    end_time:datetime \
    duration:integer \
    category_id:integer \
    event_id:integer

 scaffoldで作ると、VMC全部作ってくれます。楽ちん。
event_idはどう使うかイメージできていないけど、放送終了まではユニークなコードらしいので録画スケジュールとかで使えるかと。

後で、ChannelとProgramのchannel_idが関係付けられる感じ。Categoryも同じ。


 index作成
モデル作成時に出来たmigrationファイルに書き加えてもいいけど、区別した方が自分は好きなので。

$ rails g migration AddIndexToPrograms

db/migration/[TIME]_add_index_to_programs.rbにadd_indexを書いていく
 def change
    add_index :programs, :channel_id
    add_index :programs, :title
    add_index :programs, :category_id
    add_index :programs, :start_at
    add_index :programs, :end_at
    add_index :programs, :event_id
    add_index :programs, [:start_at,:end_at]
    add_index :programs, [:start_at,:duration]
    add_index :programs, [:category_id,:start_at]
 end
な感じ? ありそうなクエリをひと通りインデクス作っておこう。マスターレコードのChannelやCategoryも一応ね。

データベース作成
$ bundle exec rake db:create
$ bundle exec rake db:migrate


viewを確認(データ入ってないけど)
$ rails s -b 0.0.0.0

ブラウザで http://shuttle:3000/programs へ

Listing Programs

Channel_id Title Detail Start_at End_at Duration Category_id

New Program
とりあえず出ましたね。

おお。パスをわざと間違えてみたら、RoutingErrorページが格好いいじゃないですか。
多分これDevelopmentで動かしている時に出るんでしょうね。Rails3はこんなじゃなかったなぁ。
ちょっと感動。

データベースの中身が入ってこないとつまらないんで、次はそっちから片付けます。

2015/07/08

rvm + rails4 でサーバーアプリ開発環境

いつもの流れではあるので特出すべきことはないですが・・・
Shuttleで録画システム再開発ということで、前回の反省点を踏まえながら進めていく。

まずは、rvmとrails4.2 railsは仕事では3までしか使ってないんで4は自分的に初!

rvm

$ sudo apt-get intall curl

-- ここからは全部自分環境なので sudo じゃないよ --

$ curl -sSL https://rvm.io/mpapis.asc | gpg --import -
$ curl -sSL https://get.rvm.io | bash -s stable
$ source /home/shuttle/.rvm/scripts/rvm ← shuttleというユーザ名でやってます
$ rvm list known ←選択可能なrubyバージョンを確認
$ rvm install ruby-2.2.1 --default

これが長いんだ・・・暫し待つ。
この時点で gemとgem基本パッケージが入っている

rails4.2

$ gem i rails --no-ri --no-doc

これも長いよねぇ・・・


アプリ作成

$ rails new recman ←という名のアプリ作成
$ cd recman
$ bundle install
$ rails s

とりあえずWebrickでHTTPサーバー起動するかな?

あぁ、そうだった。developmentでWebrick起動するためには、'therubyracer'が必要でした。
Gemfileに、gem 'therubyracer' を追記。

もう一度
$ bundle install
$ rails s

別のPCから、http:://shuttle:3000/ へアクセス。およ?アクセス出来ない・・・?

最初は、ufw とか考えたけれど、いやいやEnforceしてないし・・・結局調べたら
何と rack version 1.6から仕様が変わったらしく、デフォルトが http://localhost:3000 となるらしい。
要するに自分自身以外からのアクセスが出来ないと。

これだと困るので

$ rails s -b 0.0.0.0

ってやると、どこからでもアクセスできるようになる。むぅ、ちょっとめんどくさいな。
rails4.2環境は初めてなので、こんなところでも引っかかってしまう。

2015/07/07

epgdump と ffmpeg + x264 + aac 再び

2年前にやったことだけれど Shuttle でも同じように使いたいので、
まずは標準的なセットアップをしておく。

epgdump

系統が幾つかあるようなのだが、json使いたいんで、こっちを使ってみる
$ git clone https://github.com/Piro77/epgdump.git

cmakeいるらしい
$ sudo apt-get install cmake

ビルド
$ cd epgdump
$ cmake .
$ make

xml,json,csv共にやってみた。良好である。

ffmpeg

こっちは後で手を加えていきたいところだけど、今は標準的なビルドをしておく

参考。英文だけど、情報に無駄がない
https://trac.ffmpeg.org/wiki/CompilationGuide/Ubuntu

取得
$ git clone https://github.com/FFmpeg/FFmpeg.git

必要最低限のライブラリインストール
$ sudo apt-get install yasm libx264-dev libfaac-dev
yasm入れておかないとcpuエンコ速くなりません。

コンフィグ実行
./configure \
  --enable-gpl \
  --enable-libfaac \
  --enable-libx264 \
  --enable-nonfree

ビルド & インストール
$ make
$ sudo make install
/usr/local/binにffmpeg,ffprobe,ffserverが。その他/usr/local/に開発用ファイルが入る。

テスト
$ recfsusb2n -b -i hd 21 30 test.ts
$ ffmpeg -y -i test.ts -c:v libx264 -c:a libfaac -preset superfast -f mp4 -threads 0 test.mp4

はい。滞りなく。

qsv対応?
nvencのIntelCPU版みたいなやつの組み込みで高速化とかもいずれ。


さあ、ここから新しい録画システム作りの始まりです。

recfsusb2n --http と リモートVLC再生

$ recfsusb2n -b -H 8888
でデーモン起動して、愛機iMacのVLCから’http://shuttle:8888/27'とかで再生されるはず。
と思ったけど若干手を加える必要があった。

VLCでアクセスした時に、recfsusb2n内の'gethostbyaddr'関数でコケてしまい
デーモンが終了してしまう。

fsusb2n.cpp内の問題箇所をざっくりコメントアウト
#if 0
                        peer_host = gethostbyaddr((char *)&peer_sin.sin_addr.s_addr, sizeof(peer_saain.sin_addr), AF_INET);
                        if ( peer_host == NULL ){
fprintf(stderr, "gethostbyname failed\n");
                                exit(1);
                        }

                        fprintf(stderr,"connect from: %s [%s] port %d\n", peer_host->h_name, inet_ntoa(peer_sin.sin_addr), ntohs(peer_sin.sin_port));
#endif
どこからアクセスされたかを表示したいだけの箇所らしいので、特に不要。

この修正を加えたrecfsusb2nを再度ビルドして、デーモン起動して、他のPCからのVLCアクセスで無事再生されました。

よかった〜

VLCのネットワークストリーム再生で、http:://shuttle:8888/24とかで綺麗に再生される。
iPadのVLCやVLC for Android、VLC for Fireでも綺麗に再生された。
それ以外の亜流品はダメだった。

2015/07/03

recfsusb2n 再び

ShuttleDS57Uで録画環境も復活させよう。
2年ぶりの再構築だけれど一度やったことだからスムースか?と思ったら、色々世の中が変化したようで・・・
 
色々コンパイルとかしないとならないので開発ツール一式インストールします
$ sudo apt-get update
$ sudo apt-get install build-essential

手持ちの KTV-FSUSB2 をShuttleで使えるようにするために recfsusb2n を再度入手。
いつの間にかGitHubに置かれたのね。

 $ git clone https://github.com/sh0/recfsusb2n.git

むむ。Makefileがない。

http://d.hatena.ne.jp/katauna/searchdiary?word=*[recfsusb2n]
こんな感じで探して、 recfsusb2n_http_patch2.zip を入手。こっちにMakefileがある。
っていうか http ストリーム配信が出来るようにするパッチでもある。
後でこの機能使おうっと

ブラウザからじゃないとダウンロード出来なかったので、Shuttleにはscpで移す。手順が微妙・・
後は
http://arkouji.cocolog-nifty.com/blog/2015/01/raspberry-piktv.html とか
http://www.lisa.jp/index.php/Epgrec を参考に

ざっとこんな感じかな

$ unzip recfsusb2n_http_patch2.zip -d recfsusb2n/src

$ cd recfsusb2n/src
Makefileを
 LIBS    = -lpthread -lboost_system -lboost_thread -lboost_filesystem
 #LIBS   = -lpthread -lboost_thread-mt -lboost_filesystem
に変更。
他にも色々変更する記事を拝見しますが、自分は必要最小限のこれだけにした。
そもそも何でこれやらなくちゃいけないかというと、ubuntuのパッケージにboost_thread-mtが無いからですな。

boostライブラリはでかいんで必要なやつだけ入れる

$ sudo apt-get install libboost-thread-dev libboost-filesystem-dev
・・・ってやっても結局
$ sudo apt-get install libboost-dev
ってやったのと同じだけ入ってしまうな。しょうがない。

$ make
何か色々ワーニングは出るけど、無事ビルド完了。


etckeeperとかTimeCapsuleマウントとか


せっかく買ったおもちゃ(ShuttleDS57U)でいろいろやる前に、きれいな状態が保てるように幾つか環境整備

sudo apt-get install git etckeeper
gitとetckeeperを入れて/etcをgitでリポジトリ管理しておこう。これからセットアップを進めていく上でロールバックできる状態にして、困ったことにならないように。

ただ、install etckeeperするとbzrで設定されてしまうので
/etc/etckeeper/etckeeper.confにbzrではなくgitを使うように書き換えて

sudo etckeeper uninit
sudo etckeeper init
する。これでbzrではなくgitでリポジトリ管理してくれる。最初なので
sudo etckeeper commit -m "first commit"
で初期状態をコミットしておく。

2015/07/02

Shuttle DS5700U 買ってみた Ubuntu14.04LTS入れていく

すごい久しぶりに新しいハードを購入した。買ったのはShuttle DS5700U ってやつ。ホームページにはDS57Uだけどダイレクトショップに行くとDS5700Uってことに。

何年か前にWavecastというセットトップボックスPCを買っていろいろ遊んでいたのだけれど、2年くらい使ったところでハードディスクの調子が悪くなってから電源入れなくなってしまった。ガーガー騒がしくてね。

ということで今度はファンレスの無音PCでまたいろいろやってみようかなと思ったので、
DS57U、Memory4GB、SSD-HDD 128GBの構成で49950円で購入。
以前のWavecastがMemory1GBで38位だったのを考えると超リーズナブル!

LANx2とかシリアルx2とか、ちょっといらないかなぁってのも付いてるんでいずれ何かの計測サーバーにでもしてみようかね。