├── .gitignore ├── src ├── public │ ├── nav.css │ ├── overlaid.css │ └── style.css ├── views │ ├── kill.slim │ ├── fil_listD.slim │ ├── cate_color.slim │ ├── prg_dialog.slim │ ├── pack_chk_view.slim │ ├── nav.sass │ ├── rsv_list_D.slim │ ├── ch_tbl_list.slim │ ├── rsv_conf.rb │ ├── opt_dialog.rb │ ├── fil_testrun.slim │ ├── move_top.html │ ├── fil_listD.rb │ ├── control.rb │ ├── cate_color.rb │ ├── overlaid.sass │ ├── top.slim │ ├── pack_chk_view.rb │ ├── rsv_conf.slim │ ├── opt_dialog.js │ ├── fil_res_dsp.slim │ ├── log_view.slim │ ├── fil_testrun.rb │ ├── config.slim │ ├── opt_dialog.slim │ ├── ch_tbl_list.rb │ ├── ch_tbl.slim │ ├── prg_tbl.slim │ ├── mpv_mon.slim │ ├── ch_info.rb │ ├── prg_dialog.rb │ ├── fil_res_dsp.rb │ ├── test.slim │ ├── ch_info.slim │ ├── tooltip.js │ ├── log_view.rb │ ├── rsv_list.slim │ ├── rsv_list_old.slim │ ├── mpv_mon.rb │ ├── control.slim │ ├── rsv_list.rb │ ├── monitor.slim │ ├── style.sass │ ├── monitor.rb │ ├── fil_list.slim │ ├── top.rb │ ├── fil_res_dsp.js │ ├── rsv_tbl.slim │ ├── layout.slim │ └── rsv_list_D.rb ├── db │ ├── run.sh │ ├── UpdateChk.rb │ ├── Keyval.rb │ ├── FilterResult.rb │ ├── PTOption.rb │ ├── Filter.rb │ ├── Phchid.rb │ ├── DB.rb │ ├── Log.rb │ ├── Category.rb │ └── base.rb ├── tool │ ├── tailLog.rb │ ├── getStatus.rb │ ├── ts2hls_sample.sh │ ├── channelScan.rb │ ├── json2text.rb │ └── reserve_SL.rb ├── Makefile ├── lib │ ├── httpd_sub.rb │ ├── YamlWrap.rb │ ├── rewriteConst.rb │ ├── setConst.rb │ ├── misc.rb │ └── deviceChk.rb ├── model │ ├── EpgLock.rb │ ├── ChannelM.rb │ ├── LogRote.rb │ ├── DiskKeep.rb │ ├── Daily.rb │ ├── Control.rb │ ├── EpgNearCh.rb │ ├── PacketChk.rb │ ├── Const.rb │ └── DelayedEPG.rb ├── require.rb ├── TV │ ├── Arguments.rb │ ├── misc.rb │ └── Video.rb └── timer_main.rb ├── EpgPatch └── extra.rb ├── doc ├── getStatus.md ├── BS再編ログ │ ├── 2021-06-01.txt │ ├── 2020-03-31.txt │ ├── 2021-02-02.txt │ ├── 2021-04-13.txt │ └── 2021-02-09.txt ├── jquery_local.md ├── TS_file_name.md ├── 動作確認環境.md ├── channelScan.md ├── reserve_SL.md ├── recpt1_px4.patch ├── Install.md └── config.md ├── Readme.md └── raspirec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/epg.db 2 | .ruby-version 3 | .gitignore 4 | -------------------------------------------------------------------------------- /src/public/nav.css: -------------------------------------------------------------------------------- 1 | nav { 2 | background-color: #2da052; } 3 | -------------------------------------------------------------------------------- /src/views/kill.slim: -------------------------------------------------------------------------------- 1 | 2 | div this is kill 3 | 4 | javascript: 5 | setTimeout(function () { window.location.href = "/"; }, 2500); 6 | -------------------------------------------------------------------------------- /src/views/fil_listD.slim: -------------------------------------------------------------------------------- 1 | / 2 | / 3 | / 4 | 5 | - require_relative 'fil_listD.rb' 6 | - dp = FilterListD.new( @id ) 7 | 8 | p 9 | | 「#{dp.title}」 を削除しても良いですか? 10 | 11 | -------------------------------------------------------------------------------- /EpgPatch/extra.rb: -------------------------------------------------------------------------------- 1 | # 2 | # raspirec 向け recpt1 使用時の BS slot 補正データ 3 | # 4 | 5 | ExtraPatch = { 6 | "BS9_2" => "BS9_1", 7 | "BS15_3" => "BS15_2", 8 | "BS23_3" => "BS23_2", 9 | } 10 | -------------------------------------------------------------------------------- /src/views/cate_color.slim: -------------------------------------------------------------------------------- 1 | 2 | - require_relative 'cate_color.rb' 3 | - dp = Cate_color.new( ) 4 | 5 | doctype html 6 | 7 | div 8 | table style="width:30em;" 9 | == dp.printTable() 10 | -------------------------------------------------------------------------------- /src/views/prg_dialog.slim: -------------------------------------------------------------------------------- 1 | 2 | 3 | - require_relative 'prg_dialog.rb' 4 | - dp = Dialog_pid.new( @pid ) 5 | 6 | doctype html 7 | 8 | div 9 | table.striped 10 | == dp.printTable() 11 | 12 | -------------------------------------------------------------------------------- /src/views/pack_chk_view.slim: -------------------------------------------------------------------------------- 1 | / 2 | / packet chk log の表示 3 | / 4 | 5 | 6 | - require_relative 'pack_chk_view.rb' 7 | - dp = Pack_chk_view.new( @rid ) 8 | 9 | 10 | == dp.print( ) 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/views/nav.sass: -------------------------------------------------------------------------------- 1 | nav 2 | //background-color: #483d8b 3 | //background-color: sandybrown 4 | //background-color: royalblue 5 | //background-color: yellowgreen 6 | //background-color: #a0522d 7 | //background-color: #522da0 8 | background-color: #2da052 9 | -------------------------------------------------------------------------------- /src/views/rsv_list_D.slim: -------------------------------------------------------------------------------- 1 | / 2 | / 予約一覧 詳細ダイアログ 3 | / 4 | 5 | 6 | - require_relative 'rsv_list_D.rb' 7 | - dp = ReservationListD.new( ) 8 | - data = dp.getData( @rid ) 9 | 10 | div 11 | form id="form" 12 | table.striped 13 | == dp.printTable( @rid ) 14 | 15 | -------------------------------------------------------------------------------- /src/views/ch_tbl_list.slim: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # 4 | 5 | css: 6 | td,th { 7 | padding: 5px ; 8 | } 9 | .skip { 10 | color: #991155 ; 11 | } 12 | 13 | - require_relative 'ch_tbl_list.rb' 14 | - pt = ChTblList.new( ) 15 | 16 | table.striped 17 | == pt.printTable() 18 | -------------------------------------------------------------------------------- /src/db/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "X$1" = "X-r" ] 4 | then 5 | #rm -f $HOME/develop/data/raspirec/db/epg.db 6 | rm -f /data/shiiya/rrtest/db/epg.db 7 | shift 8 | fi 9 | 10 | if cd $HOME/develop/raspirec 11 | then 12 | if [ "X$1" = "X-g" ] 13 | then 14 | ruby db/rec.rb 15 | fi 16 | 17 | ruby db/epgData.rb 18 | fi 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/tool/tailLog.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # log の tail 6 | # 7 | 8 | require 'optparse' 9 | require 'json' 10 | 11 | base = File.dirname( $0 ) 12 | [ ".", "..", "src",base + "/.."].each do |dir| 13 | if test(?f,dir + "/require.rb") 14 | $: << dir 15 | end 16 | end 17 | require 'require.rb' 18 | 19 | 20 | tailLog( ) 21 | 22 | -------------------------------------------------------------------------------- /src/views/rsv_conf.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 予約確認 6 | # 7 | 8 | 9 | class ReservationConfirm 10 | 11 | def initialize( ) 12 | end 13 | 14 | # 15 | # form のパラメータ設定 16 | # 17 | def setPara( ) 18 | d = {} 19 | d[:dirs] = Commlib::datalist_dir() 20 | d[:jitanchk] = true 21 | d 22 | end 23 | 24 | 25 | end 26 | 27 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | 2 | PROGRAM = /usr/bin/sass 3 | 4 | OBJS = public/nav.css public/overlaid.css public/style.css 5 | all: $(OBJS) 6 | 7 | public/nav.css: views/nav.sass 8 | $(PROGRAM) --no-cache --sourcemap=none $< $@ 9 | 10 | public/overlaid.css: views/overlaid.sass 11 | $(PROGRAM) --no-cache --sourcemap=none $< $@ 12 | 13 | public/style.css: views/style.sass 14 | $(PROGRAM) --no-cache --sourcemap=none $< $@ 15 | 16 | 17 | -------------------------------------------------------------------------------- /doc/getStatus.md: -------------------------------------------------------------------------------- 1 | 2 | ## 名前 3 | 4 | tool/getStatus.rb 5 | 6 | ## 書式 7 | 8 | getStatus.rb   [ -t n ] 9 | 10 | ## 説明 11 | 12 | 指定した時間(デフォルトは 600秒)の間、録画が始まらないかを判定して 13 | 終了ステータスを返す。 14 | 15 | | 終了ステータス | 意味| 16 | |----|-------| 17 | | 0 | 安全 | 18 | | -1 | busy | 19 | 20 | ## オプション 21 | 22 | -t n   23 | 時間の指定。単位は秒 24 | 25 | 26 | ## 注意 27 | * 事前に config.rb にて recpt1,epgdump の設定がされている事が必要です。 28 | -------------------------------------------------------------------------------- /src/views/opt_dialog.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 番組表オプション 6 | # 7 | class Dialog_opt 8 | 9 | attr_reader :sp, :hp, :tt, :hn, :chflag 10 | 11 | def initialize( session ) 12 | 13 | @chflag = session["from"] =~ /ch_tbl/ ? true : false 14 | 15 | pto = PTOption.new 16 | @sp = pto.sp 17 | @hp = pto.hp 18 | @hn = pto.hn 19 | @tt = pto.tt 20 | end 21 | 22 | end 23 | 24 | -------------------------------------------------------------------------------- /src/lib/httpd_sub.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | def makePrgtblUrl( session ) 5 | 6 | if session[:from] != nil 7 | return session[:from] 8 | else 9 | if session[:band] != nil and session[:day] != nil and session[:time] != nil 10 | return "/prg_tbl/#{session[:band]}/#{session[:day]}/#{session[:time]}" 11 | elsif session[:band] != nil 12 | return "/prg_tbl/#{session[:band]}" 13 | else 14 | return "/prg_tbl" 15 | end 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /doc/BS再編ログ/2021-06-01.txt: -------------------------------------------------------------------------------- 1 | 2 | ・実施内容 3 | トランスポンダの移動 4 | https://www.apab.or.jp/2020-bs-band-reorganization-schedule.html 5 | 6 | ディズニー・チャンネル BS03 → BS23 7 | 8 | 2021年06月01日(火)の早朝(午前0時~午前6時) 9 | 10 | ・2021/06/01 以後の slot 構成(予想) 11 | 12 | BS03_0 WOWOWプライム 13 | BS03_1 NHKBSプレミアム 14 | BS03_2 ディズニーch → 未使用 15 | 16 | BS23_0 未使用 → ディズニーch 17 | BS23_1 未使用 18 | 19 | ・raspirec への影響 20 | 21 | BS03_2 → BS23_0 ならば自動で対応出来る筈。 22 | 23 | ・結果 24 | 25 | raspirec + recpt1 は問題なし。 26 | recdvb は、チャンネル情報の修正が必要。-> 修正済み 27 | 28 | -------------------------------------------------------------------------------- /src/views/fil_testrun.slim: -------------------------------------------------------------------------------- 1 | / 2 | / フィルター条件 検索結果 3 | / 4 | 5 | - require_relative 'fil_testrun.rb' 6 | - fi = FilterTest.new( @id, @parms, session ) 7 | 8 | 9 | table.striped 10 | tr 11 | th.nowrap No 12 | th.nowrap 放送局 13 | th.nowrap 日付 14 | th.nowrap 時間 15 | th.nowrap 予約 16 | th.nowrap タイトル 17 | th.nowrap 概要 18 | == fi.testrun(@params) 19 | 20 | div id="sample-dialog" title="詳細" style="display:none;" 21 | p 22 | 23 | div id="sample-dialog2" title="確認" style="display:none;" 24 | p 25 | 26 | == Commlib::include( SrcDir + "/views/fil_res_dsp.js" ) 27 | -------------------------------------------------------------------------------- /src/db/UpdateChk.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | require 'fileutils' 5 | 6 | class DBupdateChk 7 | 8 | def initialize( ) 9 | if test(?f, DbupdateFN ) 10 | @mtime = File.mtime( DbupdateFN ) 11 | else 12 | @mtime = Time.at(0) 13 | end 14 | end 15 | 16 | def touch() 17 | FileUtils.touch(DbupdateFN) 18 | end 19 | 20 | def update?() 21 | if test(?f, DbupdateFN ) 22 | mtime = File.mtime( DbupdateFN ) 23 | if mtime > @mtime 24 | @mtime = mtime 25 | return true 26 | end 27 | end 28 | false 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /src/views/move_top.html: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | -------------------------------------------------------------------------------- /src/views/fil_listD.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # フィルター削除 ダイアログ 6 | # 7 | 8 | 9 | class FilterListD 10 | 11 | attr_reader :title 12 | 13 | def initialize( id ) 14 | @id = id 15 | @title = "" 16 | 17 | DBaccess.new().open do |db| 18 | filter = DBfilter.new() 19 | data = filter.select( db, id: @id ) 20 | if data.size > 0 21 | d = data.first 22 | if d[:title] == nil or d[:title] == "" 23 | @title = d[:key] 24 | else 25 | @title = d[:title] 26 | end 27 | end 28 | end 29 | end 30 | 31 | 32 | end 33 | 34 | -------------------------------------------------------------------------------- /src/model/EpgLock.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # 4 | # 多重起動防止 5 | # 6 | 7 | module EpgLock 8 | 9 | def lock?() 10 | if test( ?f, EPGLockFN ) 11 | now = Time.now 12 | mtime = File.mtime( EPGLockFN ) 13 | if mtime > ( now - 3600 * 1) # 1時間以内 14 | #DBlog::sto( "Error: Lock file exist") 15 | return true 16 | end 17 | end 18 | false 19 | end 20 | module_function :lock? 21 | 22 | def lock() 23 | FileUtils.touch(EPGLockFN) 24 | end 25 | module_function :lock 26 | 27 | def unlock() 28 | if test(?f, EPGLockFN ) 29 | File.unlink( EPGLockFN ) 30 | end 31 | end 32 | module_function :unlock 33 | 34 | end 35 | 36 | 37 | -------------------------------------------------------------------------------- /doc/jquery_local.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## jquery と materializ をローカルに配置して使う方法 4 | 5 | - 下記のファイルを public の下にダウンロードする。 6 | 7 | BaseDir はインストールしたディレクトリ 8 | 9 | ``` 10 | % cd $BaseDir/src/public 11 | % wget https://code.jquery.com/jquery-3.3.1.min.js 12 | % wget https://code.jquery.com/ui/1.12.0/jquery-ui.min.js 13 | % wget http://code.jquery.com/ui/1.12.1/themes/pepper-grinder/jquery-ui.css 14 | % wget https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css 15 | % wget https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js 16 | ``` 17 | 18 | - $HOME/.config/raspirec/config.rb の下記のパラメータを true に変更する。 19 | 20 | `Local_jquery = true` 21 | 22 | - プログラムを再起動する。 23 | 24 | 25 | -------------------------------------------------------------------------------- /doc/TS_file_name.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## TS ファイル名の生成ルール 4 | 5 | - 出力する TS ファイル名の生成ルールは、config.rb 中の TSnameFormat で指定する。 6 | 7 | - デフォルトは、 8 | "%YEAR%-%MONTH%-%DAY%_%HOUR%:%MIN%_%DURATION%_%TITLE%_%CHNAME%" 9 | で、
10 | "2019-12-17_15:00_1800_ショッピング情報_BS11イレブン.ts" に展開される。 11 | 12 | - 使用出来るキーワードは下記のもの。 13 | 14 | |キーワード |意味 | 15 | |-------------|--------------| 16 | |%TITLE% | 番組タイトル | 17 | |%ST% | 開始日時( YYYYMMDDHHMM ) | 18 | |%ET% | 終了日時(同上) | 19 | |%BAND% | GR,BS,CS | 20 | |%CHNAME% | 放送局名 | 21 | |%YEAR% | 開始年 | 22 | |%MONTH% | 開始月 | 23 | |%DAY% | 開始日 | 24 | |%HOUR% | 開始時 | 25 | |%MIN% | 開始分 | 26 | |%SEC% | 開始秒 | 27 | |%WDAY% | 曜日 0(日曜日)から6(土曜日) | 28 | |%DURATION% | 録画時間(秒) | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/views/control.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # コントロール パネル 6 | # 7 | require 'find' 8 | 9 | class Control 10 | 11 | def initialize( ) 12 | end 13 | 14 | # 15 | # from の初期値 16 | # 17 | def getData( ) 18 | ret = { :tsft => false } 19 | keyval = DBkeyval.new 20 | DBaccess.new().open do |db| 21 | row = keyval.select( db, "tsft" ) 22 | if row == "true" 23 | ret[ :tsft ] = true 24 | end 25 | end 26 | 27 | ts = [] 28 | Find.find( TSDir ) do |f| 29 | if f =~ /\.ts$/ 30 | f.sub!( TSDir + "/", '') 31 | ts << sprintf("",f,f) 32 | end 33 | end 34 | ret[:tsfile] = ts.join("\n") 35 | 36 | return ret 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /src/views/cate_color.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # カテゴリの配色表 6 | # 7 | 8 | 9 | class Cate_color 10 | 11 | def initialize( ) 12 | 13 | end 14 | 15 | def printTable2( title, val, id = nil) 16 | color = sprintf("color%d",val ) 17 | sprintf(%Q{ %s %s },title, color) 18 | end 19 | 20 | 21 | def printTable() 22 | r = [] 23 | 24 | row = nil 25 | category = DBcategory.new 26 | DBaccess.new(DbFname).open do |db| 27 | row = category.selectL( db ) 28 | if row != nil and row.size > 0 29 | row.each do |tmp| 30 | r << printTable2( tmp[:name], tmp[:id] ) 31 | end 32 | end 33 | end 34 | r.join("\n") 35 | end 36 | end 37 | 38 | -------------------------------------------------------------------------------- /src/lib/YamlWrap.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # 6 | # YAML の非互換性を吸収するためのラッパー 7 | # 8 | 9 | module YamlWrap 10 | 11 | def load_file( fname ) 12 | # pp "load_file() " + Gem::Version.new( Psych::VERSION ).to_s 13 | if Gem::Version.new( Psych::VERSION ) < Gem::Version.new( "4.0.0" ) 14 | return YAML.load_file( fname ) 15 | else 16 | return YAML.unsafe_load_file( fname ) 17 | end 18 | end 19 | 20 | def load( fname ) 21 | # pp "load() " + Gem::Version.new( Psych::VERSION ).to_s 22 | if Gem::Version.new( Psych::VERSION ) < Gem::Version.new( "4.0.0" ) 23 | return YAML.load( fname ) 24 | else 25 | return YAML.unsafe_load( fname ) 26 | end 27 | end 28 | 29 | module_function :load_file 30 | module_function :load 31 | 32 | end 33 | 34 | -------------------------------------------------------------------------------- /doc/動作確認環境.md: -------------------------------------------------------------------------------- 1 | 2 | ## 動作確認環境 3 | 4 | ### 録画機側 5 | 6 | #### その1 7 | 8 | * raspberry pi 3B+ 9 | * Raspbian Stretch 10 | * PX-Q3U4 11 | * px4_drv 12 | * ruby 2.3.3p222 13 | * メモ 14 | * 録画は 6本同時までは可 15 | * 動作速度は、遅いが問題なく使える 16 | 17 | 18 | #### その2 19 | 20 | * raspberry pi zero w 21 | * Raspbian Buster 22 | * PX-Q3U4 23 | * px4_drv 24 | * ruby 2.5 25 | * メモ 26 | * 録画は2本同時までは可 27 | * 動作速度は、かろうじて動くぐらい。 28 | * Wi-Fi 接続の為、転送速度は 2Mbyte/秒 程度で、実用に耐えない。 29 | 30 | #### その 3 31 | 32 | * AMD Ryzen 7 2700 + MEM 16G 33 | * Ubuntu 18.04.3 LTS 34 | * PT2 35 | * pt1_drv 36 | * ruby 2.5 37 | * メモ 38 | * 録画は 8本以上可 39 | * 動作速度は、問題なく使える 40 | 41 | ### ブラウザ側 42 | 43 | * linux版 opera 44 | * 問題なし 45 | * linux版 firefox 46 | * 問題なし 47 | * android版 firefox 48 | * 一部のアクションが起きない不具合あり。 49 | * android版 opera 50 | * ドロップダウンリストが選択出来ない不具合あり。 51 | -------------------------------------------------------------------------------- /doc/channelScan.md: -------------------------------------------------------------------------------- 1 | 2 | ## 名前 3 | 4 | tool/channelScan.rb 地デジ チャンネルスキャン 5 | 6 | ## 書式 7 | 8 | channelScan.rb [ -t n ] 9 | 10 | ## 説明 11 | 12 | 地上デジタル放送の帯域をチャンネルスキャンし、 13 | 放送波のあるチャンネルを表示します。 14 | 15 | ## オプション 16 | 17 | -t n   18 | 1チャンネル当たりの検出時間を n 秒にします。デフォルトは 10秒です。 19 | 20 | ## 例 21 | 22 | ``` 23 | % ruby channelScan.rb 24 | # 10 : 0.0dB 25 | # 11 : 0.0dB 26 | # 12 : 0.0dB 27 | # 13 : 22.2dB GR0_30728 GR0_30730 NHKEテレ1長野 NHKEテレ3長野 28 | # 14 : 34.6dB GR0_30736 テレビ信州1 29 | # 15 : 41.1dB GR0_30760 長野放送 30 | # 16 : 31.5dB GR0_30752 SBC信越放送1 31 | # 17 : 25.7dB GR0_30720 NHK総合1・長野 32 | # 18 : 21.7dB GR0_30744 長野朝日放送 33 | # 19 : 0.0dB 34 | ... 35 | GR_EPG_channel = %w( 13 14 15 16 17 18 ) 36 | 37 | ``` 38 | 39 | 最終行が config.rb に設定する値になります。 40 | 41 | ## 注意 42 | * 事前に config.rb にて recpt1,epgdump の設定がされている事が必要です。 43 | -------------------------------------------------------------------------------- /src/model/ChannelM.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 6 | # 7 | 8 | 9 | class ChannelM 10 | 11 | def initialize( ) 12 | 13 | end 14 | 15 | # 16 | # skip の設定 17 | # 18 | def set( chid, skip ) 19 | 20 | DBaccess.new().open(tran: true) do |db| 21 | channel = DBchannel.new 22 | val = skip == "on" ? 1 : 0 23 | channel.updateSkip( db, val, chid ) 24 | end 25 | end 26 | 27 | # 28 | # 削除 29 | # 30 | def delete( chid ) 31 | 32 | DBaccess.new().open(tran: true) do |db| 33 | channel = DBchannel.new 34 | channel.delete( db, chid ) 35 | end 36 | end 37 | 38 | 39 | # 40 | # 無効化 41 | # 42 | def invalid( chid ) 43 | 44 | DBaccess.new().open( tran: true ) do |db| 45 | channel = DBchannel.new 46 | channel.invalid( db, chid ) 47 | end 48 | end 49 | 50 | 51 | end 52 | 53 | -------------------------------------------------------------------------------- /src/tool/getStatus.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 録画サーバーが指定した時間に稼働中か判定する。 6 | # 0 -> wait 7 | # 0以外 -> busy 8 | 9 | require 'optparse' 10 | 11 | base = File.dirname( $0 ) 12 | [ ".", "..","src", base + "/..", ].each do |dir| 13 | if test(?f,dir + "/require.rb") 14 | $: << dir 15 | end 16 | end 17 | require 'require.rb' 18 | 19 | # 20 | # 引数の解析 21 | # 22 | $opt = { 23 | :t => 600, # チェックする時間 24 | } 25 | 26 | OptionParser.new do |opt| 27 | opt.on('-t n') {|v| $opt[ :t ] = v.to_i } 28 | opt.parse!(ARGV) 29 | end 30 | 31 | if EpgLock::lock?() == false 32 | timer = Timer.new 33 | ( queue, recCount, nextRecTime ) = timer.getNextProg() 34 | 35 | sa = nextRecTime - Time.now.to_i 36 | if recCount > 0 or sa < $opt[:t] 37 | exit -1 38 | end 39 | else 40 | exit -1 41 | end 42 | 43 | exit 0 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/views/overlaid.sass: -------------------------------------------------------------------------------- 1 | nav 2 | height: 48px 3 | line-height: 48px 4 | i, [class^="mdi-"], [class*="mdi-"], i.material-icons, .button-collapse i 5 | height: 48px 6 | line-height: 48px 7 | .brand-logo 8 | font-size: 1.6rem 9 | 10 | .tabs 11 | height: 38px 12 | 13 | .tabs .tab a 14 | padding: 6px 5px 12px 5px 15 | height: 38px 16 | width: 6em 17 | 18 | .row .col 19 | padding-left: 5px 20 | padding-right: 5px 21 | 22 | #bands 23 | padding-left: 8px 24 | padding-right: 8px 25 | 26 | 27 | @media only screen and (min-width: 601px) 28 | nav 29 | height: 48px 30 | line-height: 48px 31 | .nav-wrapper i 32 | height: 48px 33 | line-height: 48px 34 | a.button-collapse 35 | height: 48px 36 | line-height: 48px 37 | i 38 | height: 48px 39 | line-height: 48px 40 | 41 | .row 42 | margin-bottom: 10px 43 | 44 | -------------------------------------------------------------------------------- /src/public/overlaid.css: -------------------------------------------------------------------------------- 1 | nav { 2 | height: 48px; 3 | line-height: 48px; } 4 | nav i, nav [class^="mdi-"], nav [class*="mdi-"], nav i.material-icons, nav .button-collapse i { 5 | height: 48px; 6 | line-height: 48px; } 7 | nav .brand-logo { 8 | font-size: 1.6rem; } 9 | 10 | .tabs { 11 | height: 38px; } 12 | 13 | .tabs .tab a { 14 | padding: 6px 5px 12px 5px; 15 | height: 38px; 16 | width: 6em; } 17 | 18 | .row .col { 19 | padding-left: 5px; 20 | padding-right: 5px; } 21 | 22 | #bands { 23 | padding-left: 8px; 24 | padding-right: 8px; } 25 | 26 | @media only screen and (min-width: 601px) { 27 | nav { 28 | height: 48px; 29 | line-height: 48px; } 30 | nav .nav-wrapper i { 31 | height: 48px; 32 | line-height: 48px; } 33 | nav a.button-collapse { 34 | height: 48px; 35 | line-height: 48px; } 36 | nav a.button-collapse i { 37 | height: 48px; 38 | line-height: 48px; } } 39 | .row { 40 | margin-bottom: 10px; } 41 | -------------------------------------------------------------------------------- /doc/BS再編ログ/2020-03-31.txt: -------------------------------------------------------------------------------- 1 | 2 | ・実施内容 3 | 4 | Dlife, FOX スポーツ&エンターテイメント が 2020/03/31 で停波 5 | 6 | 7 | ・2020/03/31 以前の slot 構成 8 | 9 | BS23_0 BS釣りビジョン 10 | BS23_1 BS日本映画専門チャンネル 11 | BS23_2 Dlife 12 | 13 | BS11_0 FOX スポーツ&エンターテイメント 14 | BS11_1 BSスカパー 15 | BS11_2 放送大学 16 | 17 | ・2020/03/31 以前の slot 構成 18 | 19 | BS23_0 BS釣りビジョン 20 | BS23_1 BS日本映画専門チャンネル 21 | 22 | BS11_0 BSスカパー 23 | BS11_1 放送大学 24 | 25 | ・raspirec への影響 26 | 27 | * Dlife の方は単純に無くなっただけなので問題なし。 28 | * Fox は無くなって、BSスカパー,放送大学のスロットが移動したはずなのに、EPG 29 | の情報は古いままなので、BSスカパー,放送大学が正常に受信出来ない。 30 | 31 | 停波前のSlot 停波後のSlot 停波後のEPG 32 | BS11_0 FOX BSスカパー 空き 33 | BS11_1 BSスカパー 放送大学 BSスカパー <- ズレ 34 | BS11_2 放送大学 空き 放送大学 <- ズレ 35 | 36 | 推測だが市販のTVチュナーは、EPGデータの上位に空きスロットがある場 37 | 合は 自動で slot番号を詰める機能を有していて問題ない模様。 38 | 39 | 従って、EPG のデータを強制的に読み替える機能がどこかに必要。 40 | -> Ver0.4.0 で機能追加し解決 41 | 42 | -------------------------------------------------------------------------------- /doc/reserve_SL.md: -------------------------------------------------------------------------------- 1 | 2 | ## 名前 3 | 4 | tool/reserve_SL.rb 予約データの save & load 5 | 6 | ## 書式 7 | 8 | reserve_SL.rb [Options1] [Options2...] file 9 | 10 | ## 説明 11 | 12 | データベース中の予約関係データを保存、読み込みを行う。 13 | 14 | ## オプション 15 | 16 | ### Options1 17 | 18 | -s,--save   19 | データセーブモード : DBの中の予約関係データ を yaml 形式でダンプする。 20 | 21 | -l,--load   22 | データロードモード : ダンプしたファイルを DBに読み込む。 23 | 24 | ### Options2 25 | 26 | -f, --filter   27 | フィルターデータ 28 | 29 | -a, --auto   30 | 自動予約データ 31 | 32 | -r, --reserv   33 | 未録画 予約データ 34 | 35 | -o, --old   36 | 録画済み 予約データ 37 | 38 | -A, --ALL   39 | 全部(-f,-a,-r,-o) 40 | 41 | -d, --db db_file   42 | DBファイルの指定(デフォルトは config.rb 中の DbFname ) 43 | 44 | -C, --clearTable   45 | 読み込む前にデータ削除 46 | 47 | file   yaml 入力(-l時),出力(-s時) ファイル名 48 | 49 | ## 例 50 | 51 | ``` 52 | % ruby tool/reserve_SL.rb --save -A backup.yaml # バックアップ 53 | % ruby tool/reserve_SL.rb --load -f -a backup.yaml # restore 54 | ``` 55 | 56 | 57 | 58 | ## 注意 59 | * 事前に config.rb の設定がされている事が必要です。 60 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | ## 目的 3 | 4 | 本プログラム(raspirec) は Linux 系OS上で、 5 | recpt1/recdvb,epgdump を使って TV番組を録画する録画サーバーを構築するための 6 | 録画予約システムです。 7 |
8 | 特に、ラズパイのようなシングルボードコンピュータ(SBC)で動作させる事に最適化しています。 9 | 10 | なお動作には recpt1/recdvb に対応しているドライバーがあるTVチューナー 11 | ( アースソフト社製 PT1〜3, PLEX社製 PX-W3U4、PX-W3PE4、PX-Q3U4、PX-Q3PE4 等) 12 | が必要です。 13 | 14 | ## 特徴 15 | 16 | * Raspberry Pi のようなシングルボードコンピュータ(SBC)でも実行できるように、 17 | 機能はシンプルで小型、軽量。 18 | * 処理能力の低いPCでも動作するように最適化。 19 | * 同時録画本数を多くする為に録画中はなるべく負荷を掛けない。 20 | (raspbery Pi 3B+ で 6本までは確認) 21 | * インストールが容易 22 | * WEBインターフェースで、EPG番組表から録画予約が可能。 23 | * 条件検索を設定することで、自動予約登録が可能。 24 | * 録画したTSファイルを、親機にコピーする機能 25 | * チューナーの数とは別に、同時録画の本数で制限を掛ける事が可能。 26 | (SBCの場合、能力不足により本数制限が必要になる。) 27 | 28 | 29 | ## 詳細 30 | インストール方法等の詳細は、 31 | [GitHub Pages](https://kaikoma-soft.github.io/src/raspirec.html) 32 | を参照して下さい。 33 | 34 | 35 | ## docker 36 | [dockerによる動作テスト環境](https://github.com/kaikoma-soft/docker-raspirec) 37 | を用意したので、簡単に動作テストを行う事が出来ます。 38 | 39 | 40 | 41 | ## ライセンス 42 | このソフトウェアは、Apache License Version 2.0 ライセンスのも 43 | とで公開します。詳しくは LICENSE を見て下さい。 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/views/top.slim: -------------------------------------------------------------------------------- 1 | 2 | css: 3 | .tbl { 4 | position: absolute; 5 | top: 0px; 6 | right: 0px; 7 | bottom: 0px; 8 | left: 0px; 9 | margin: auto; 10 | 11 | width: 800px; 12 | height: 550px; 13 | } 14 | 15 | - require_relative 'top.rb' 16 | - top = Top.new.setup 17 | 18 | .card.tbl 19 | .card-content 20 | span#title.card-title ステータス表示 21 | table 22 | tr 23 | td.nowrap 状況 24 | td.nowrap #{top[:stat]} 25 | tr 26 | td.nowrap 現/次番組名 27 | td.nowrap #{{top[:stat2]}} 28 | tr 29 | td.nowrap Disk 使用量/全容量 30 | td.nowrap #{top[:diskUsed]} / #{top[:diskTotal]} ( #{top[:diskWari]} ) 31 | tr 32 | td.nowrap 予約数 33 | td.nowrap #{top[:reserveNum]} 34 | tr 35 | td.nowrap チューナー不足数 36 | td.nowrap 37 | == top[:ConflictNum] 38 | tr 39 | td.nowrap 過去3日間の録画エラー数 40 | td.nowrap 41 | == top[:ErrorNum] 42 | tr 43 | td.nowrap プログラム 44 | td.nowrap #{Const::ProgName} #{Commlib::getVer()} 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/views/pack_chk_view.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 6 | # 7 | class Pack_chk_view 8 | 9 | attr_reader :sp, :hp, :tt, :hn, :chflag 10 | 11 | def initialize( rid ) 12 | @rid = rid 13 | 14 | end 15 | 16 | def print 17 | 18 | logfname = nil 19 | DBaccess.new().open do |db| 20 | reserve = DBreserve.new() 21 | r = reserve.select( db, id: @rid ) 22 | l = r.first 23 | path = TSDir + "/" 24 | if l[:subdir] != nil and l[:subdir] != "" 25 | subdir2 = Commlib::normStr( l[:subdir] ) 26 | path += subdir2.sub(/^\//,'').sub(/\/$/,'').strip + "/" 27 | end 28 | path += l[:fname] 29 | logfname = path + ".chk" 30 | end 31 | 32 | r = [] 33 | r << "
"
34 |     if logfname != nil
35 |       if test(?f, logfname )
36 |         File.open( logfname, "r" ) do |fpr|
37 |           fpr.each_line do |line|
38 |             r << line.chomp
39 |           end
40 |         end
41 |       else
42 |         r << "file not found\n#{logfname}"
43 |       end
44 |     end
45 |     r << "
" 46 | r.join("\n") 47 | end 48 | 49 | end 50 | 51 | -------------------------------------------------------------------------------- /src/tool/ts2hls_sample.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # 4 | # hls 変換スクリプト 5 | # 6 | # パラメータの受け渡しは、下記の環境変数で行う 7 | # 8 | # SOURCE_CMD チューナーの起動コマンド 9 | # SOURCE_OUT "-" or ffmpeg 入力ファイル名 10 | # STREAM_DIR 出力先ディレクトリ 11 | # PLAYLIST PLAYLIST 出力ファイル名 12 | 13 | #PATH=/usr/bin:/usr/local/bin:/bin:$PATH 14 | 15 | #set -x 16 | 17 | if cd $STREAM_DIR 18 | then 19 | exec > ts2hls.log 2>&1 20 | 21 | $SOURCE_CMD "$SOURCE_OUT" | \ 22 | ffmpeg \ 23 | -re \ 24 | -i pipe:0 \ 25 | -r 10 \ 26 | -bsf:v h264_mp4toannexb \ 27 | -movflags faststart \ 28 | -max_muxing_queue_size 1024 \ 29 | -analyzeduration 10M -probesize 10M \ 30 | -map 0:v:0 -map 0:a -ignore_unknown \ 31 | -c:v h264 -g 10 \ 32 | -c:a copy \ 33 | -flags +cgop+global_header \ 34 | -f hls \ 35 | -hls_time 5 -hls_list_size 3 -hls_allow_cache 0 \ 36 | -hls_flags delete_segments \ 37 | -hls_segment_filename stream_%05d.ts \ 38 | "$PLAYLIST" \ 39 | > ffmpeg.log 2>&1 40 | else 41 | echo "dir not found $STREAM_DIR " 42 | fi 43 | 44 | # -vf "fps=10,yadif=0:-1:1" 45 | -------------------------------------------------------------------------------- /doc/BS再編ログ/2021-02-02.txt: -------------------------------------------------------------------------------- 1 | 2 | ・実施内容 3 | トランスポンダの移動 4 | https://www.apab.or.jp/topics/2020/2101070001.html 5 | 6 | BS 19ch→21ch グリーンチャンネル 7 | BS 21ch→19ch J SPORTS 4 8 | 9 | 2021年2月2日(火)の早朝(午前0時~午前7時) 10 | リーンチャンネルは(午前0時~午前6時59分) 11 | 12 | ・2021/02/02 以前の slot 構成 13 | 14 | BS19_0 グリーンチャンネル 15 | BS19_1 J SPORTS 1 16 | BS19_2 J SPORTS 2 17 | 18 | BS21_0 WOWOWプラス 19 | BS21_1 J SPORTS 3 20 | BS21_2 J SPORTS 4 21 | 22 | ・2021/02/02 以後の slot 構成(予想) 23 | 24 | BS19_0 J SPORTS 4 25 | BS19_1 J SPORTS 1 26 | BS19_2 J SPORTS 2 27 | 28 | BS21_0 WOWOWプラス 29 | BS21_1 J SPORTS 3 30 | BS21_2 グリーンチャンネル 31 | 32 | ・raspirec への影響 33 | 34 | EPGデータが変更になったら、それを元に自動でDBを書き換えるので対応は 35 | 不要の筈。 36 | 37 | ・結果 38 | 39 | 予定通り EPG 取得後に channel情報変更がされ問題なし。 40 | 41 | 07:08:05: channel情報変更 グリーンチャンネル tsid 18224 -> 18258 42 | 07:08:05: channel情報変更 グリーンチャンネル stinfo_tp BS19 -> BS21 43 | 07:08:05: channel情報変更 グリーンチャンネル stinfo_slot 0 -> 2 44 | 07:08:05: channel情報変更 J SPORTS 4 tsid 18258 -> 18224 45 | 07:08:05: channel情報変更 J SPORTS 4 stinfo_tp BS21 -> BS19 46 | 07:08:05: channel情報変更 J SPORTS 4 stinfo_slot 2 -> 0 47 | 48 | -------------------------------------------------------------------------------- /doc/BS再編ログ/2021-04-13.txt: -------------------------------------------------------------------------------- 1 | 2 | ・実施内容 3 | トランスポンダの移動 4 | https://www.apab.or.jp/2020-bs-band-reorganization-schedule.html 5 | 6 | BS釣りビジョン BS 23ch→11ch 7 | 日本映画専門チャンネル BS 23ch→21ch 8 | 9 | 2021年4月13日(火)の早朝(午前0時~午前6時) 10 | 11 | ・2021/04/13 以後の slot 構成(予想) 12 | 13 | BS23_0 BS釣りビジョン 14 | BS23_1 日本映画専門ch 15 | 16 | BS11_0 BSスカパー! 17 | BS11_1 放送大学 18 | BS11_2 空き -> BS釣りビジョン ? 19 | 20 | BS21_0 WOWOWプラス 21 | BS21_1 空き -> 日本映画専門チャンネル 22 | BS21_2 グリーンチャンネル 23 | 24 | ・raspirec への影響 25 | 26 | BS釣りビジョンが BS11_0 に入る可能性もあるので、その場合は現在当て 27 | ているパッチを外す必要あり。(BSスカパーの放送休止があるので可能性大?) 28 | 29 | 日本映画専門チャンネルは多分 BS21_1 に入るので、自動で対応出来る筈。 30 | 31 | ・結果 32 | 33 | BS11_0 BSスカパー!(EPG は BS11_1) 34 | BS11_1 放送大学 (EPG は BS11_2) 35 | BS11_2 空き -> BS釣りビジョン (EPG は BS11_3) 36 | 37 | BS21_0 WOWOWプラス 38 | BS21_1 空き -> 日本映画専門チャンネル (BS21_1) 39 | BS21_2 グリーンチャンネル 40 | 41 | 日本映画専門チャンネルは問題なく自動対応。 42 | BS釣りビジョンは BS11_2 に入ったが、BS11_* の EPG 上のスロット番号 43 | と実際に選局するものとズレを引き継いでしまっている。 44 | よって、EpgPatch を追加しなければならない。-> Ver1.0.3 で対応 45 | 46 | -------------------------------------------------------------------------------- /src/db/Keyval.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class DBkeyval 6 | 7 | include Base 8 | 9 | def initialize( ) 10 | @list = { 11 | key: "key", 12 | val: "val", 13 | } 14 | @para = [] 15 | @list.each_pair { |k,v| @para << v } 16 | @tbl_name = "keyval" 17 | end 18 | 19 | # 20 | # 格納 21 | # 22 | def insert(db, key, val ) 23 | data = { key: key, val: val } 24 | ( sql, args ) = makeInsSql( @list, @para, @tbl_name, data ) 25 | db.execute( sql, *args ) 26 | end 27 | 28 | 29 | # 30 | # 検索 31 | # 32 | def select( db, key ) 33 | sql = "select val from #{@tbl_name} where key = ? ;" 34 | row = db.execute( sql, key) 35 | if row != nil and row[0] != nil 36 | return row[0][0] 37 | end 38 | nil 39 | end 40 | 41 | 42 | # 43 | # 削除 44 | # 45 | def delete( db, key ) 46 | sql = "delete from #{@tbl_name} where key = ? ;" 47 | db.execute( sql, key ) 48 | end 49 | 50 | # 51 | # 更新 52 | # 53 | def upsert( db, key, val ) 54 | sql = "INSERT OR REPLACE INTO #{@tbl_name} (key, val) VALUES (?,?) ;" 55 | db.execute( sql, key, val ) 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /src/views/rsv_conf.slim: -------------------------------------------------------------------------------- 1 | / 2 | / 録画予約登録 確認 3 | / 4 | 5 | css: 6 | #item { 7 | width:8em; 8 | } 9 | #title { 10 | margin-bottom: 2em; 11 | } 12 | #submit { 13 | margin-top: 1em; 14 | } 15 | .nowrap { 16 | white-space: nowrap; 17 | } 18 | th { 19 | padding-right: 1em; 20 | } 21 | #in,#email_inline { 22 | height: 2em; 23 | } 24 | #email_inline { 25 | width: 50%; 26 | margin-left: 1em; 27 | } 28 | span { color: #000000} 29 | 30 | - require_relative 'rsv_conf.rb' 31 | - dp = ReservationConfirm.new( ) 32 | - d = dp.setPara( ) 33 | 34 | 35 | form id="form" method="POST" action="/rsv/add" 36 | table.striped 37 | tr 38 | th.nowrap オプション 39 | td.nowrap 40 | .row 41 | .col 42 | | 保存サブディレクトリ 43 | .col 44 | input#email_inline( type="text" list="dlist" name="dir" size="60" class="validate" value="" autocomplete="off" ) 45 | datalist#dlist 46 | | #{{d[:dirs]}} 47 | .row 48 | label 49 | input.filled-in( type="checkbox" name="jitan" checked=(d[:jitanchk])) 50 | span#cbcomm チューナーが競合した場合に録画時間の短縮を許可する。 51 | 52 | -------------------------------------------------------------------------------- /src/db/FilterResult.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class DBfilterResult 6 | 7 | include Base 8 | 9 | def initialize( ) 10 | @list = { 11 | id: "id", 12 | pid: "pid", 13 | rid: "rid", 14 | } 15 | @para = [] 16 | @list.each_pair { |k,v| @para << v } 17 | @tbl_name = "filter_result" 18 | end 19 | 20 | # 21 | # 格納 22 | # 23 | def insert(db, pid, rid ) 24 | data = { id: nil, pid: pid, rid: rid } 25 | ( sql, args ) = makeInsSql( @list, @para, @tbl_name, data ) 26 | 27 | db.execute( sql, *args ) 28 | end 29 | 30 | 31 | # 32 | # 検索 33 | # 34 | def select(db, pid: nil, tend: nil ) 35 | ( sql, args ) = makeSelectSql( @para,@tbl_name, pid: pid, tend: tend ) 36 | 37 | row = db.execute( sql, *args ) 38 | row2hash( @list, row ) 39 | end 40 | 41 | 42 | # 43 | # 削除 44 | # 45 | def delete( db, id ) 46 | sql = "delete from filter_result where pid = ? ;" 47 | db.execute( sql, id ) 48 | end 49 | 50 | # 51 | # 更新 52 | # 53 | def update( db, data, id ) 54 | ( sql, args ) = makeUpdSql( @list, @tbl_name, data, id ) 55 | 56 | db.execute( sql, *args ) 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /src/db/PTOption.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # 番組表のオプション のload/save 4 | # 5 | class PTOption 6 | 7 | SP = "StationPage" 8 | HP = "HourPixel" 9 | HN = "HourNum" 10 | TT = "ToolTip" 11 | 12 | attr_reader :sp, :hp, :tt, :hn 13 | 14 | def initialize( ) 15 | keyval = DBkeyval.new 16 | DBaccess.new().open do |db| 17 | @sp = keyval.select( db, SP ) 18 | @hp = keyval.select( db, HP ) 19 | @hn = keyval.select( db, HN ) 20 | @tt = keyval.select( db, TT ) 21 | end 22 | 23 | @sp = StationPage if @sp == nil 24 | @hp = 180 if @hp == nil 25 | @hn = 6 if @hn == nil 26 | if @tt == nil 27 | @tt = true 28 | else 29 | @tt = @tt == 1 ? true : false 30 | end 31 | end 32 | 33 | def save( para ) 34 | hp = para["range1"] 35 | hn = para["range2"] 36 | sp = para["range3"] 37 | tt = para["tooltip"] == "on" ? 1 : 0 38 | 39 | keyval = DBkeyval.new 40 | DBaccess.new().open do |db| 41 | @sp = keyval.upsert( db, SP, sp ) if sp != nil 42 | @hp = keyval.upsert( db, HP, hp ) if hp != nil 43 | @hn = keyval.upsert( db, HN, hn ) if hn != nil 44 | @tt = keyval.upsert( db, TT, tt ) if tt != nil 45 | end 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /doc/BS再編ログ/2021-02-09.txt: -------------------------------------------------------------------------------- 1 | 2 | ・実施内容 3 | トランスポンダの移動 & スロット縮減 4 | https://www.apab.or.jp/topics/2020/2101070001.html 5 | 6 | BS 21ch→19ch J SPORTS 3 7 | 8 | 2021年2月9日(火)の早朝(午前0時~午前7時) 9 | 10 | ・2021/02/09 以前の slot 構成 11 | 12 | BS19_0 J SPORTS 4 13 | BS19_1 J SPORTS 1 14 | BS19_2 J SPORTS 2 15 | 16 | BS21_0 WOWOWプラス 17 | BS21_1 J SPORTS 3 18 | BS21_2 グリーンチャンネル 19 | 20 | ・2021/02/09 以後の slot 構成(予想) 21 | 22 | BS19_0 J SPORTS 4 23 | BS19_1 J SPORTS 1 24 | BS19_2 J SPORTS 2 25 | BS19_3 J SPORTS 3 26 | 27 | BS21_0 WOWOWプラス 28 | BS21_1 グリーンチャンネル 29 | 30 | ・raspirec への影響 31 | 32 | グリーンチャンネルの EPGでの slot番号がどうなるか(2のままか 1に移行する 33 | か)で、パッチを当てる可能性がある。( 2020/03 の Fox のパターン ) 34 | 35 | 2021/02/09 以前 2021/02/09 以後 2021/02/09 以後のEPG 36 | BS21_0 WOWOWプラス 同左 同左 37 | BS21_1 J SPORTS 3 グリーンチャンネル 空き 38 | BS21_2 グリーンチャンネル 未使用 グリーンチャンネル 39 | 40 | ・結果 41 | 42 | 予想とは違いグリーンチャンネル は BS21_2 のままなので、通常の EPG 更 43 | 新時に行われる J SPORTS 3 のチャンネル情報の変更だけで済み。 44 | 45 | 2021-02-09 06:52:36 channel情報変更 J SPORTS 3 stinfo_slot 1 -> 3 46 | 2021-02-09 06:52:36 channel情報変更 J SPORTS 3 stinfo_tp BS21 -> BS19 47 | 2021-02-09 06:52:36 channel情報変更 J SPORTS 3 tsid 18257 -> 18227 48 | 49 | ・疑問点 50 | 今回と 2020/03 の Fox の場合は何が違うのか? 51 | Fox は 0番slot だから? 52 | -------------------------------------------------------------------------------- /src/lib/rewriteConst.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # DevAutoDetection で、定数を書き換え 4 | # 5 | 6 | def constRewrite( name, val) 7 | sym = name.to_sym 8 | base = File.basename($0) 9 | if Object.const_defined?(sym) == true 10 | Object.send(:remove_const, sym) 11 | end 12 | DBlog::sto("Const rewrite #{base} #{name} = #{val}") 13 | Object.const_set(sym, val ) 14 | end 15 | 16 | 17 | # 18 | # チューナーデバイスの自動設定 19 | # 20 | if Object.const_defined?(:RewriteConst) == true and 21 | RewriteConst == true and 22 | Object.const_defined?(:DevAutoDetection) == true and 23 | DevAutoDetection == true 24 | if ( data = DeviceChk.new.load()) != nil 25 | if data.total > 0 26 | constRewrite( "DeviceList_GR", data.listGR ) 27 | constRewrite( "DeviceList_BSCS", data.listBC ) 28 | constRewrite( "DeviceList_GBC", data.listGBC ) 29 | else 30 | DBlog::error(nil,"デバイスの自動検出に失敗しました。 DevAutoDetection を false にして、手動で設定して下さい。") 31 | end 32 | end 33 | end 34 | 35 | # # 36 | # # EpgPatchEnable = :auto の場合の自動設定 37 | # # 38 | # if ( Object.const_defined?(:EpgPatchEnable) == true ) && 39 | # EpgPatchEnable == :auto 40 | # if ( Object.const_defined?(:Recpt1_cmd) == true ) 41 | # if Recpt1_cmd =~ /recdvb/ 42 | # constRewrite( "EpgPatchEnable", false ) 43 | # else 44 | # constRewrite( "EpgPatchEnable", true ) 45 | # end 46 | # end 47 | # end 48 | -------------------------------------------------------------------------------- /src/model/LogRote.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 予約一覧 6 | # 7 | require 'sys/filesystem' 8 | 9 | class LogRote 10 | 11 | Limit = 100 * 1000 # 100k byte 以上 12 | Max = 5 # 残すのは 5世代 13 | 14 | def initialize( ) 15 | @flist = [ 16 | LogFname, 17 | StdoutM, 18 | StderrM, 19 | StdoutH, 20 | StderrH, 21 | StdoutT, 22 | StderrT, 23 | ] 24 | 25 | end 26 | 27 | # 28 | # log ローテーションが必要か? 29 | # 30 | def need?() 31 | @flist.each do |fn| 32 | if test( ?f, fn ) 33 | size = File.size( fn ) 34 | if size > Limit 35 | return true 36 | end 37 | end 38 | end 39 | false 40 | end 41 | 42 | def exec() 43 | Max.downto( 1 ) do |n| 44 | dir = sprintf("%s/old_%02d",LogDir, n ) 45 | if test( ?d, dir ) 46 | if n == Max 47 | FileUtils.rm_r( dir ) 48 | else 49 | to = sprintf("%s/old_%02d",LogDir, n +1 ) 50 | File.rename( dir, to ) 51 | end 52 | end 53 | end 54 | to = sprintf("%s/old_%02d",LogDir, 1 ) 55 | Dir.mkdir( to ) 56 | @flist.each do |fn| 57 | base = File.basename(fn) 58 | to = sprintf("%s/old_%02d/%s",LogDir, 1,base ) 59 | File.rename( fn, to ) 60 | FileUtils.touch( fn ) 61 | end 62 | end 63 | end 64 | 65 | -------------------------------------------------------------------------------- /src/require.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | require 'pp' 5 | require 'sqlite3' 6 | require 'fileutils' 7 | require 'json' 8 | require 'etc' 9 | 10 | require 'lib/setConst.rb' # 最初に 11 | 12 | require 'lib/httpd_sub.rb' 13 | require 'lib/commlib.rb' 14 | Commlib::makeSubDir() 15 | 16 | require 'lib/misc.rb' 17 | require 'db/DB.rb' 18 | require 'db/base.rb' 19 | require 'db/Category.rb' 20 | require 'db/Channel.rb' 21 | require 'db/EpgTable.rb' 22 | require 'db/Filter.rb' 23 | require 'db/FilterResult.rb' 24 | require 'db/Programs.rb' 25 | require 'db/Reserve.rb' 26 | require 'db/Keyval.rb' 27 | require 'db/Log.rb' 28 | require 'db/UpdateChk.rb' 29 | require 'db/Phchid.rb' 30 | require 'db/PTOption.rb' 31 | 32 | require 'lib/rewriteConst.rb' # DB の後に 33 | 34 | require 'model/Const.rb' 35 | require 'model/Tuner.rb' 36 | require 'model/TunerArray.rb' 37 | require 'model/TunerAssign2.rb' 38 | require 'model/FilterM.rb' 39 | require 'model/Reservation.rb' 40 | require 'model/Timer.rb' 41 | require 'model/Recpt1.rb' 42 | require 'model/EpgLock.rb' 43 | require 'model/Control.rb' 44 | require 'model/FileCopy.rb' 45 | require 'model/DiskKeep.rb' 46 | require 'model/LogRote.rb' 47 | require 'model/ChannelM.rb' 48 | require 'model/Monitor.rb' 49 | require 'model/PacketChk.rb' 50 | require 'model/EpgNearCh.rb' 51 | require 'model/GetEPG.rb' 52 | require 'model/Daily.rb' 53 | require 'model/EpgAutoPatch.rb' 54 | -------------------------------------------------------------------------------- /doc/recpt1_px4.patch: -------------------------------------------------------------------------------- 1 | --- recpt1/recpt1/pt1_dev.h 2019-09-24 20:29:13.818064078 +0900 2 | +++ recpt1-px4/recpt1/pt1_dev.h 2019-09-24 20:30:08.485287187 +0900 3 | @@ -3,32 +3,28 @@ 4 | #define _PT1_DEV_H_ 5 | 6 | char *bsdev[NUM_BSDEV] = { 7 | - "/dev/pt1video1", 8 | "/dev/pt1video0", 9 | - "/dev/pt1video5", 10 | - "/dev/pt1video4", 11 | - "/dev/pt1video9", 12 | - "/dev/pt1video8", 13 | - "/dev/pt1video13", 14 | - "/dev/pt1video12", 15 | - "/dev/pt3video1", 16 | - "/dev/pt3video0", 17 | - "/dev/pt3video5", 18 | - "/dev/pt3video4" 19 | + "/dev/pt1video1", 20 | + "/dev/px4video0", 21 | + "/dev/px4video1", 22 | + "/dev/px4video4", 23 | + "/dev/px4video5", 24 | + "/dev/px4-DTV0", 25 | + "/dev/px4-DTV1", 26 | + "/dev/px4-DTV4", 27 | + "/dev/px4-DTV5", 28 | }; 29 | char *isdb_t_dev[NUM_ISDB_T_DEV] = { 30 | "/dev/pt1video2", 31 | "/dev/pt1video3", 32 | - "/dev/pt1video6", 33 | - "/dev/pt1video7", 34 | - "/dev/pt1video10", 35 | - "/dev/pt1video11", 36 | - "/dev/pt1video14", 37 | - "/dev/pt1video15", 38 | - "/dev/pt3video2", 39 | - "/dev/pt3video3", 40 | - "/dev/pt3video6", 41 | - "/dev/pt3video7" 42 | + "/dev/px4video2", 43 | + "/dev/px4video3", 44 | + "/dev/px4video6", 45 | + "/dev/px4video7", 46 | + "/dev/px4-DTV2", 47 | + "/dev/px4-DTV3", 48 | + "/dev/px4-DTV6", 49 | + "/dev/px4-DTV7", 50 | }; 51 | 52 | // 変換テーブル(ISDB-T用) 53 | -------------------------------------------------------------------------------- /src/views/opt_dialog.js: -------------------------------------------------------------------------------- 1 | 2 | 41 | -------------------------------------------------------------------------------- /src/views/fil_res_dsp.slim: -------------------------------------------------------------------------------- 1 | / 2 | / フィルター条件 検索結果 3 | / 4 | 5 | - require_relative 'fil_res_dsp.rb' 6 | - fd = FilterDisp.new( @params ) 7 | - r = fd.print( @id, session ) 8 | 9 | css: 10 | th { 11 | padding-right: 1em; 12 | } 13 | td,th { 14 | padding: 8px ; 15 | } 16 | .list-inline { 17 | padding-left: 0; 18 | margin-left: -5px; 19 | list-style: none; 20 | } 21 | .list-inline > li { 22 | display: inline-block; 23 | padding-right: 5px; 24 | padding-left: 5em; 25 | } 26 | label { color: #000000} 27 | td {white-space: nowrap; } 28 | 29 | form 30 | ul.list-inline 31 | li #{{r[:title]}} 32 | - if fd.filter?() 33 | li 34 | label 35 | input.cb type="checkbox" name="freeOnly" id="cb" checked=(fd.fo?) 36 | span 予約済みは表示しない 37 | 38 | table.striped 39 | tr 40 | th No 41 | th 放送局 42 | th 日付 43 | th 時間 44 | th 予約 45 | th タイトル 46 | th 概要 47 | | #{{r[:table]}} 48 | 49 | div id="sample-dialog" title="詳細" style="display:none;" 50 | p 51 | 52 | div id="sample-dialog2" title="確認" style="display:none;" 53 | p 54 | 55 | == Commlib::include( SrcDir + "/views/move_top.html" ) 56 | 57 | == Commlib::include( SrcDir + "/views/fil_res_dsp.js" ) 58 | 59 | javascript: 60 | 61 | $(function(){ 62 | $('.cb').on('change', function () { 63 | if ($(this).prop('checked')) { 64 | var url="/fil_res_dsp/#{@id}" +"?FO=on"; 65 | } else { 66 | var url="/fil_res_dsp/#{@id}"; 67 | } 68 | window.location.href = url ; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/views/log_view.slim: -------------------------------------------------------------------------------- 1 | / 2 | / ログの表示 3 | / 4 | 5 | css: 6 | td,th { 7 | padding: 5px ; 8 | } 9 | label { 10 | margin: 10px ; 11 | color: black; 12 | font-size: 1em; 13 | } 14 | .inline-block { 15 | display: inline-block; /* インラインブロック要素にする */ 16 | padding: 0px; 17 | margin: 0px 2px 0px 2px; 18 | } 19 | .pagination { 20 | margin-left: 3em; 21 | } 22 | 23 | - require_relative 'log_view.rb' 24 | - dp = LogView.new( @level, @page ) 25 | - radio = dp.radio() 26 | 27 | div 28 | form 29 | label 表示レベル 30 | label 31 | input( type="radio" name="level" value="1" checked=(radio[:debug])) 32 | span DEBUG 33 | label 34 | input(type="radio" name="level" value="2" checked=(radio[:info])) 35 | span 情報 36 | label 37 | input(type="radio" name="level" value="3" checked=(radio[:atte])) 38 | span 注意 39 | label 40 | input(type="radio" name="level" value="4" checked=(radio[:warn])) 41 | span 警告 42 | label 43 | input(type="radio" name="level" value="5" checked=(radio[:err])) 44 | span エラー 45 | == dp.pageSel() 46 | 47 | p 48 | table.striped 49 | tr 50 | th.nowrap レベル 51 | th.nowrap 日時 52 | th.nowrap 内容 53 | == dp.printTable() 54 | 55 | == Commlib::include( SrcDir + "/views/move_top.html" ) 56 | 57 | javascript: 58 | $(function(){ 59 | $( 'input[name="level"]:radio' ).change( function() { 60 | var radioval = $(this).val(); 61 | //console.log(radioval) 62 | window.location.href = "/log_view/" + radioval 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /doc/Install.md: -------------------------------------------------------------------------------- 1 | 2 | ### インストール例 3 | 4 | #### 条件 5 | 6 | PC raspberrypi zero w 7 | TVチューナー PLEX PX-Q3U4 8 | OS raspbian-buster-lite 9 | ドライバー px4_drv 10 | 11 | 12 | #### インストール 13 | 14 | - インストールに必要なパッケージを apt install で インストール 15 | 16 | ``` 17 | git 18 | autoconf 19 | raspberrypi-kernel-headers 20 | dkms 21 | cmake 22 | sqlite3 23 | ruby 24 | ruby-sqlite3 25 | ruby-sys-filesystem 26 | ruby-net-ssh 27 | ruby-sinatra 28 | ruby-slim 29 | ruby-sass 30 | ``` 31 | 32 | - recpt1 33 | 34 | ``` 35 | git clone https://github.com/stz2012/recpt1.git 36 | cd recpt1/recpt1 37 | sh autogen.sh 38 | ./configure 39 | PX-Q3U4 用に デバイス名を変更する。( recpt1_px4.patch 参照 ) 40 | make 41 | sudo make install 42 | ``` 43 | 44 | - px4_drv 45 | 46 | `git clone https://github.com/nns779/px4_drv.git` 47 | 48 | ドキュメントに従ってインストール 49 | 50 | 51 | - epgdump 52 | 53 | ``` 54 | git clone https://github.com/Piro77/epgdump 55 | cd epgdump 56 | cmake . 57 | make 58 | sudo make install 59 | ``` 60 | 61 | 62 | - raspirec 本体 63 | 64 | インストールするディレクトリに移動して 65 | 66 | ``` 67 | git clone https://github.com/kaikoma-soft/raspirec.git 68 | cd raspirec 69 | mkdir -p ~/.config/raspirec 70 | cp config.rb.sample ~/.config/raspirec/config.rb 71 | エディタで、~/.config/raspirec/config.rb を適宜修正 72 | ``` 73 | 74 | - 動作確認 75 | 76 | `% ruby raspirec.rb` 77 | 78 | でプログラムを起動する。 79 | すぐに終了するがバックグランドでサービスが走っているので、 80 | 81 | `http://localhost:4567/` 82 | 83 | にアクセスし、画面が出れば正常 84 | なお初回は、EPG番組表のデータ取得するまで多少の時間が掛かる。 85 | -------------------------------------------------------------------------------- /src/views/fil_testrun.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # フィルター検索結果 6 | # 7 | 8 | 9 | class FilterTest 10 | 11 | def initialize( id, params, session ) 12 | @id = id 13 | @params = params 14 | @session = session 15 | if @session != nil 16 | @fa_type = @session["fa_type"] 17 | end 18 | end 19 | 20 | # 21 | # 試行ボタン 22 | # 23 | def testrun(params) 24 | r = [] 25 | fp = FilterM.new() 26 | d = fp.formAna( params ) 27 | programs = DBprograms.new 28 | reserve = DBreserve.new 29 | DBaccess.new().open do |db| 30 | r2 = fp.search3( db, d ) 31 | data = nil 32 | data = programs.selectSP( db, proid: r2 ) 33 | size = sprintf("%d 件",data.size ) 34 | size += "以上" if data.size == FilConst::SeachMax 35 | r << sprintf("

検索結果 %s

", size) 36 | 37 | tdclas = [ "nowrap","item" ] 38 | data.each_with_index do |t, n | 39 | (day, time, w) = Commlib::stet_to_s( t[:start], t[:end] ) 40 | cate = t[:categoryA][0][0] 41 | trcl = %W(color#{cate}) 42 | res = reserve.select( db, evid: t[:evid], chid: t[:chid] ) 43 | res2 = "" 44 | if res.size > 0 45 | if res[0][:stat] == RsvConst::Normal 46 | res2 = "○" 47 | else 48 | res2 = "×" 49 | end 50 | end 51 | data = [ n+1,t[:name], day,time, res2,t[:title],t[:detail] ] 52 | r << Commlib::printTR2( data, rid: t[:pid], trcl: trcl, tdcl: tdclas, ) 53 | end 54 | end 55 | r.join("\n") 56 | end 57 | 58 | 59 | end 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/db/Filter.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class DBfilter 6 | 7 | include Base 8 | 9 | def initialize( ) 10 | @list = { 11 | id: "id", 12 | type: "type", 13 | title: "title", 14 | key: "key", 15 | exclude: "exclude", 16 | regex: "regex", 17 | band: "band", 18 | target: "target", 19 | chanel: "chanel", 20 | category: "category", 21 | wday: "wday", 22 | result: "result", 23 | jitan: "jitan", 24 | subdir: "subdir", 25 | freeonly: "freeonly", 26 | dedupe: "dedupe", 27 | } 28 | @para = [] 29 | @list.each_pair { |k,v| @para << v } 30 | @tbl_name = "filter" 31 | end 32 | 33 | # 34 | # 35 | # 36 | def delete( db, id ) 37 | sql = "delete from filter where id = ? ;" 38 | db.execute( sql, id ) 39 | end 40 | 41 | 42 | # 43 | # 条件検索の取得 44 | # 45 | def select( db, id: nil, type: nil ) 46 | ( sql, args ) = makeSelectSql( @para, @tbl_name, id: id, type: type) 47 | row = db.execute( sql, args ) 48 | row2hash( @list, row ) 49 | end 50 | 51 | # 52 | # 条件検索の追加 53 | # 54 | def insert( db, data ) 55 | 56 | ( sql, args ) = makeInsSql( @list, @para, @tbl_name, data ) 57 | db.execute( sql, *args ) 58 | end 59 | 60 | # 61 | # 条件検索の書き換え 62 | # 63 | def update( db, data, id ) 64 | 65 | ( sql, args ) = makeUpdSql( @list, @tbl_name, data, id ) 66 | db.execute( sql, *args ) 67 | end 68 | 69 | # 70 | # 検索結果の更新 71 | # 72 | def update_res(db, id, size ) 73 | sql = "update filter set result = ? where id = ?;" 74 | r = db.execute( sql, size, id ) 75 | end 76 | 77 | end 78 | -------------------------------------------------------------------------------- /src/views/config.slim: -------------------------------------------------------------------------------- 1 | 2 | css: 3 | .title { 4 | margin: 1.5em 0.5em 1.0em 0.5em ; 5 | font-size: 1.2em ; 6 | } 7 | td,th { 8 | font-size: 0.9em ; 9 | } 10 | .pad { 11 | padding-left:2em ; 12 | } 13 | ul.browser-default { 14 | padding-left: 40px; 15 | } 16 | ul.browser-default li { 17 | list-style-type: inherit; 18 | } 19 | .title2 { 20 | margin: 0.5em 0.5em 0.5em 0.5em ; 21 | } 22 | 23 | - require_relative 'config.rb' 24 | - dp = Config.new() 25 | 26 | 27 | h2.title 28 | | 目次 29 | ol 30 | li.title2 31 | a href="#init" はじめに 32 | li.title2 33 | a href="#httpd" httpd 34 | li.title2 35 | a href="#dirFile" ディレクトリ、ファイル関係 36 | li.title2 37 | a href="#recTime" 録画タイミング関係 38 | li.title2 39 | a href="#tuner" チューナー関係 40 | li.title2 41 | a href="#epg" EPG関係 42 | li.title2 43 | a href="#RecExt" 録画自動延長 44 | li.title2 45 | a href="#dlgOpt" ダイアログのオプション初期値 46 | li.title2 47 | a href="#tsft" TSファイル転送 48 | li.title2 49 | a href="#hlsMon" HLS モニタ機能 50 | li.title2 51 | a href="#mpvMon" mpv モニタ機能 52 | li.title2 53 | a href="#PacketChk" パケットチェック機能 54 | li.title2 55 | a href="#other" その他 56 | 57 | hr 58 | 59 | 60 | div 61 | ol 62 | li.title 63 | a name="init" はじめに 64 | 65 | ul.browser-default 66 | li 67 | | config ファイルは、次の優先度で読み込まれます。 68 | ol 69 | li 70 | | 環境変数 RASPIREC_CONF で指定したファイル(拡張子は .rb) 71 | li 72 | | $HOME/.config/raspirec/config.rb 73 | li 74 | | 現在読み込んでいるのは、#{ConfigPath} です。 75 | li 76 | | 配列の定義で、%w( 1 2 3 ) は [ "1", "2", "3" ] と等価です。 77 | 78 | == dp.printHTML() 79 | -------------------------------------------------------------------------------- /src/tool/channelScan.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 地デジ チャンネルスキャン 6 | # 7 | 8 | require 'optparse' 9 | require 'json' 10 | 11 | base = File.dirname( $0 ) 12 | [ ".", "..","src", base + "/..", ].each do |dir| 13 | if test( ?f, dir + "/require.rb") 14 | $: << dir 15 | end 16 | end 17 | require 'require.rb' 18 | 19 | 20 | $opt = { 21 | :d => false, # debug 22 | :w => false, # write config 23 | :t => 10, # scan time 24 | } 25 | 26 | OptionParser.new do |opt| 27 | opt.on('-d') { $opt[:d] = true } 28 | opt.on('-w') { $opt[:w] = true } 29 | opt.on('-t n') {|v| $opt[:t] = v.to_i } 30 | opt.parse!(ARGV) 31 | end 32 | 33 | 34 | class ExecError < StandardError; end 35 | 36 | 37 | GR_start = 10 38 | GR_end = 52 39 | pt1 = Recpt1.new 40 | $rec_pid = [] 41 | 42 | outdir = DataDir + "/json" 43 | unless test(?d, outdir ) 44 | Dir.mkdir( outdir ) 45 | end 46 | 47 | chs = [] 48 | GR_start.upto(GR_end) do |ch| 49 | outfname = outdir + "/#{ch}.json" 50 | now1 = Time.now 51 | cn = pt1.getEpgJson_retry( ch, $opt[:t], outfname ) 52 | now2 = Time.now 53 | 54 | name = "" 55 | id = "" 56 | if test( ?s, outfname ) 57 | File.open( outfname, "r" ) do |fp| 58 | str = fp.read 59 | data = JSON.parse(str) 60 | if data.size > 0 61 | data.each do |d| 62 | name += d["name"] + " " 63 | id += d["id"] + " " 64 | end 65 | end 66 | end 67 | end 68 | 69 | if $opt[:d] == true 70 | printf("# %02d : %03.1fdB %s %s (%d)\n",ch, cn, id, name, now2 - now1 ) 71 | else 72 | printf("# %02d : %03.1fdB %s %s\n",ch, cn, id, name ) 73 | end 74 | if name != "" 75 | chs << ch 76 | end 77 | 78 | sleep(0.1) 79 | end 80 | 81 | printf("\nGR_EPG_channel = %%w( %s )\n",chs.join(" ")) 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/views/opt_dialog.slim: -------------------------------------------------------------------------------- 1 | 2 | 3 | - require_relative 'opt_dialog.rb' 4 | - dp = Dialog_opt.new( session ) 5 | 6 | doctype html 7 | 8 | css: 9 | label { color: #000000} 10 | #form { 11 | margin-top: 2em; 12 | margin-bottom: 2em; 13 | } 14 | 15 | 16 | form( id="form" method="post" action="" ) 17 | .row 18 | .col.s1 19 | .col.s4 縦軸の長さ(1時間当たりのピクセル数) 20 | .col.s6 21 | input(type="range" id="range1" min="50" max="500" value="#{dp.hp}" step="10" name="range1") 22 | .col.s1 23 | span#value1 #{dp.hp} 24 | - if dp.chflag == false 25 | .row 26 | .col.s1 27 | .col.s4 縦軸の長さ(時間数) 28 | .col.s6 29 | input(type="range" id="range2" min="3" max="24" value="#{dp.hn}" step="1" name="range2") 30 | .col.s1 31 | span#value2 #{dp.hn} 32 | .row 33 | .col.s1 34 | .col.s4 1ページ当たりの放送局数 (個) 35 | .col.s6 36 | input(type="range" id="range3" min="1" max="60" value="#{dp.sp}" name="range3") 37 | .col.s1 38 | span#value3 #{dp.sp} 39 | .row 40 | .col.s1 41 | .col.s4 ツールチップ 42 | .col.s6 43 | label 44 | input.cb type="checkbox" name="tooltip" id="cb" checked=(dp.tt) 45 | span 表示する 46 | .col.s1 47 | 48 | 49 | javascript: 50 | var elem = document.getElementById('range1'); 51 | var target = document.getElementById('value1'); 52 | var elem2 = document.getElementById('range2'); 53 | var target2 = document.getElementById('value2'); 54 | var elem3 = document.getElementById('range3'); 55 | var target3 = document.getElementById('value3'); 56 | 57 | var rangeValue = function (elem, target) { 58 | return function(evt){ 59 | target.innerHTML = elem.value; 60 | } 61 | } 62 | elem.addEventListener('input', rangeValue(elem, target)); 63 | elem2.addEventListener('input', rangeValue(elem2, target2)); 64 | elem3.addEventListener('input', rangeValue(elem3, target3)); 65 | 66 | -------------------------------------------------------------------------------- /src/views/ch_tbl_list.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # ch 毎の番組表 6 | # 7 | 8 | 9 | class ChTblList 10 | 11 | Base = "/ch_tbl" 12 | 13 | def initialize( ) 14 | @tate = 24 15 | @ret = {} 16 | channel = DBchannel.new 17 | DBaccess.new().open() do |db| 18 | row = channel.select(db, order: "order by svid") 19 | row.each do |r| 20 | next if r[:updatetime] == -1 21 | cl = r[:skip] == 0 ? "disp" : "skip" 22 | tmp = sprintf(%Q{ %s },cl,r[:chid],r[:name]) 23 | @ret[ r[:band] ] ||= [] 24 | @ret[ r[:band] ] << tmp 25 | end 26 | end 27 | 28 | end 29 | 30 | # 31 | # 表 32 | # 33 | def printTable( ) 34 | r = [] 35 | tmp = {} 36 | %w( GR BS CS ).each do |band| 37 | n = 0 38 | tmp[ band ] ||= {} 39 | tmp[ band ][ n ] ||= [] 40 | 41 | if @ret[band] != nil and @ret[band].size > @tate 42 | @ret[band].each_slice( @tate ) do |part| 43 | tmp[ band ][ n ] ||= [] 44 | tmp[ band ][ n ] = part 45 | n += 1 46 | end 47 | else 48 | tmp[ band ][ n ] = @ret[band] 49 | end 50 | end 51 | 52 | r << "" 53 | tmp.keys.each do |band| 54 | size = tmp[band].size 55 | r << %Q( #{band} ) 56 | end 57 | r << "" 58 | 59 | while true 60 | r << "" 61 | count = 0 62 | tmp.keys.each do |band| 63 | tmp[band].keys.each do |n| 64 | if tmp[band][n] != nil 65 | d = tmp[band][n].shift 66 | if d != nil 67 | r << "" + d + "" 68 | count += 1 69 | else 70 | r << "
" 71 | end 72 | end 73 | end 74 | end 75 | r << "" 76 | break if count == 0 77 | end 78 | r.join("\n") 79 | end 80 | 81 | 82 | end 83 | -------------------------------------------------------------------------------- /src/views/ch_tbl.slim: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # 4 | # 5 | 6 | 7 | css: 8 | th { 9 | padding-right: 1em; 10 | } 11 | td,th { 12 | padding: 8px ; 13 | } 14 | .list-inline { 15 | padding-left: 0; 16 | margin-left: -5px; 17 | list-style: none; 18 | } 19 | .list-inline > li { 20 | display: inline-block; 21 | padding-right: 5px; 22 | padding-left: 5em; 23 | font-size: 1.2em ; 24 | } 25 | .l { 26 | font-size: 1.3em ; 27 | } 28 | .m { 29 | padding-left: 0.5em; 30 | font-size: 1.1em ; 31 | } 32 | label { color: #000000} 33 | 34 | - require_relative 'ch_tbl.rb' 35 | - pt = ChTbl.new( @ch ) 36 | 37 | ul.list-inline 38 | li 39 | span.l 40 | | #{pt.name} 41 | span.m 42 | | ( ch:#{pt.phch}, svid:#{pt.svid} ) 43 | li 44 | label 45 | input.cb type="checkbox" name="skip" id="cb" checked=(pt.skip) 46 | span 番組表、検索の対象から外す。 47 | li 48 | a.btn.waves-effect.waves-light.btn-small.lime.darken-2#option オプション 49 | 50 | .row 51 | div style="display:table;border-collapse: separate;border-spacing: 3px 0;" 52 | .dtr 53 | div.station style="width:2.3em;" 54 | a href="/cate_color" style="color:white;" 色 55 | == pt.yokojiku() 56 | .dtr 57 | .dtc 58 | == pt.tatejiku() 59 | == pt.prgTable( @ch ) 60 | 61 | 62 | == Commlib::include( SrcDir + "/views/move_top.html" ) 63 | == Commlib::include( SrcDir + "/views/prg_tbl.cssjs" ) 64 | == Commlib::includeIf( pt.tt, SrcDir + "/views/tooltip.js" ) 65 | 66 | div#option-dialog( style="display:none;" title="オプション" ) 67 | p 68 | 69 | == Commlib::include( SrcDir + "/views/opt_dialog.js" ) 70 | 71 | javascript: 72 | 73 | $(function(){ 74 | $('.cb').on('change', function () { 75 | var url="/ch_tbl/#{@ch}" 76 | if ($(this).prop('checked')) { 77 | url = url + "?skip=on"; 78 | } else { 79 | url = url + "?skip=off"; 80 | } 81 | window.location.href = url ; 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/TV/Arguments.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'optparse' 4 | 5 | class Arguments 6 | 7 | attr_reader :x # x座標 8 | attr_reader :y # y座標 9 | attr_reader :w # 幅 10 | attr_reader :h # 高さ 11 | attr_reader :font # font名 12 | attr_reader :d # debug level 13 | attr_reader :round # 巡回ボタン 14 | attr_reader :epg # EPG 取得 15 | 16 | def initialize(argv) 17 | @x = @y = @w = @h = @font = nil 18 | @round = nil 19 | @d = 0 20 | @epg = false 21 | 22 | if Object.const_defined?( :RaspirecTV_font ) == true 23 | @font = RaspirecTV_font 24 | end 25 | if Object.const_defined?( :RaspirecTV_GEO ) == true 26 | geoConv( RaspirecTV_GEO ) 27 | end 28 | 29 | op = option_parser 30 | op.parse!(argv) 31 | rescue OptionParser::ParseError => e 32 | $stderr.puts e 33 | exit(1) 34 | end 35 | 36 | private 37 | 38 | def option_parser 39 | OptionParser.new do |op| 40 | op.on('-g', '--geometry WxH+X+Y','座標指定(WxH+X+Y or +X+Y)') { |t| 41 | geoConv( t ) 42 | } 43 | op.on('-f', '--font font_name','font指定 (例:"Sans 14")'){ |t| 44 | @font = t 45 | } 46 | op.on('-r', '--round time',Integer,'巡回ボタン表示(time=巡回時間(秒))'){ |t| 47 | @round = t.to_i 48 | } 49 | op.on('-e', '--epg','EPG取得'){ |t| 50 | @epg = true 51 | } 52 | op.on('-d', '--debug','debug mode'){ |t| 53 | @d += 1 54 | } 55 | end 56 | end 57 | 58 | # 59 | # 座標の数値化 WxH+X+Y 60 | # 61 | def geoConv( geo ) 62 | if geo =~ /^(\d+)x(\d+)\+(\d+)\+(\d+)$/ 63 | @w, @h, @x, @y = $1.to_i,$2.to_i,$3.to_i,$4.to_i 64 | elsif geo =~ /^(\d+)\+(\d+)$/ 65 | @x, @y = $1.to_i,$2.to_i 66 | else 67 | 68 | end 69 | end 70 | 71 | end 72 | 73 | # 74 | # debug 出力 75 | # 76 | def dlog( str, level = 0 ) 77 | if $arg != nil 78 | if $arg.d > level 79 | puts( str ) 80 | end 81 | end 82 | end 83 | 84 | -------------------------------------------------------------------------------- /src/views/prg_tbl.slim: -------------------------------------------------------------------------------- 1 | 2 | css: 3 | a { 4 | color: white ; 5 | } 6 | #band { 7 | margin-left: 10px; 8 | margin-right: 10px; 9 | padding-left: 10px; 10 | padding-right: 10px; 11 | } 12 | #date { 13 | margin-left: 10px; 14 | margin-right: 10px; 15 | width: 120px; 16 | } 17 | 18 | #option { 19 | margin-left: 200px; 20 | margin-right: 10px; 21 | } 22 | label { color: #000000} 23 | 24 | 25 | - require_relative 'prg_tbl.rb' 26 | - pt = PrgTbl.new( @band, @day, @time ) 27 | - timeHref = pt.getTimeHref() 28 | - d = pt.setFormPara() 29 | 30 | 31 | .row 32 | .col 33 | == pt.dateSel() 34 | .col 35 | a.btn.waves-effect.waves-light.btn-small.green.darken-4 href="#{timeHref[0]}" 朝 36 | .col 37 | a.btn.waves-effect.waves-light.btn-small.green.darken-4 href="#{timeHref[1]}" 昼 38 | .col 39 | a.btn.waves-effect.waves-light.btn-small.green.darken-4 href="#{timeHref[2]}" 夜 40 | .col 41 | a.btn.waves-effect.waves-light.btn-small.green.darken-4 href="#{timeHref[3]}" 深夜 42 | .col 43 | a.btn.waves-effect.waves-light.btn-small.lime.darken-4 href="#{timeHref[4]}" -3H 44 | .col 45 | a.btn.waves-effect.waves-light.btn-small.lime.darken-4 href="#{timeHref[5]}" +3H 46 | .col 47 | == pt.bandSel() 48 | .col 49 | == pt.pageSel() 50 | 51 | .col 52 | a.btn.waves-effect.waves-light.btn-small.lime.darken-2#option オプション 53 | 54 | 55 | .row 56 | div style="display:table;border-collapse: separate;border-spacing: 3px 0;" 57 | .dtr 58 | div.station style="width:2.3em;" 59 | a href="/cate_color" style="color:white;" 色 60 | == pt.yokojiku() 61 | 62 | .dtr 63 | .dtc 64 | == pt.tatejiku() 65 | == pt.prgTable() 66 | 67 | == pt.nowLine() 68 | 69 | == Commlib::include( SrcDir + "/views/prg_tbl.cssjs" ) 70 | == Commlib::includeIf( pt.tt, SrcDir + "/views/tooltip.js" ) 71 | == Commlib::include( SrcDir + "/views/move_top.html" ) 72 | 73 | 74 | 79 | 80 | == Commlib::include( SrcDir + "/views/opt_dialog.js" ) 81 | -------------------------------------------------------------------------------- /src/tool/json2text.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # EPG の json を読みやすく整形して出力する。 6 | # 7 | 8 | require 'optparse' 9 | require 'fileutils' 10 | require 'json' 11 | 12 | 13 | class Json2text 14 | 15 | PRO = "programs" 16 | TITLE = "title" 17 | EVID = "event_id" 18 | CHAN = "channel" 19 | DETA = "detail" 20 | EXTD = "extdetail" 21 | START = "start" 22 | ENDT = "end" 23 | DURA = "duration" 24 | CATE = "category" 25 | STINF = "satelliteinfo" 26 | 27 | def initialize( fname, opt ) 28 | @count = 0 29 | 30 | File.open( fname, "r" ) do |fp| 31 | str = fp.read 32 | data = JSON.parse(str) 33 | if opt[:evid] != nil 34 | evid = opt[:evid].to_i 35 | data.each do |ch| 36 | if ch[ PRO ] != nil 37 | ch[ PRO ].each do |prog| 38 | if prog[ EVID ] == evid 39 | print( ch, prog ) 40 | end 41 | end 42 | end 43 | end 44 | else 45 | data.each do |ch| 46 | if ch[ PRO ] != nil 47 | ch[ PRO ].each do |prog| 48 | print( ch, prog ) 49 | end 50 | end 51 | end 52 | end 53 | end 54 | end 55 | 56 | 57 | def print( ch, prog) 58 | puts("") if @count > 0 59 | 60 | name = ch["name"] 61 | stinf = ch[STINF] != nil ? 62 | sprintf("%s_%s",ch[STINF]["TP"],ch[STINF]["SLOT"]) : 63 | "" 64 | [ CHAN, TITLE, DETA,EVID, START, ENDT, DURA ].each do |key| 65 | case key 66 | when START, ENDT 67 | val = Time.at( prog[ key ] / 1000 ).to_s 68 | when CHAN 69 | val = sprintf("%s %s %s", name, prog[ key ], stinf ) 70 | else 71 | val = prog[ key ] 72 | end 73 | printf("%10s: %s\n", key, val ) 74 | end 75 | 76 | @count += 1 77 | end 78 | 79 | end 80 | 81 | 82 | $opt = { 83 | :evid => nil, # event id 指定 84 | } 85 | 86 | OptionParser.new do |opt| 87 | opt.on('--evid ID') {|v| $opt[:evid] = v } 88 | opt.parse!(ARGV) 89 | end 90 | 91 | 92 | ARGV.each do |fname| 93 | Json2text.new( fname, $opt ) 94 | end 95 | 96 | -------------------------------------------------------------------------------- /src/views/mpv_mon.slim: -------------------------------------------------------------------------------- 1 | / 2 | / mpv モニタ 3 | / 4 | 5 | css: 6 | td,th { 7 | padding: 10px ; 8 | } 9 | th { 10 | width: 15em; 11 | } 12 | span { color: #000000} 13 | .busy { 14 | color: #f00000; 15 | } 16 | .radio { 17 | margin-right: 2em; 18 | } 19 | 20 | 21 | - require_relative 'mpv_mon.rb' 22 | - dp = MpvMon.new( @tunNum, @cmd ) 23 | - dp.createData( ) 24 | 25 | 26 | .row 27 | form id="form2" method="POST" action="/mpv_mon" 28 | table.striped 29 | tr 30 | th チューナー 31 | td 32 | == dp.deviceSelect() 33 | tr 34 | th デバイスファイル名 35 | td 36 | == dp.devfn() 37 | tr 38 | th.nowrap 39 | | 状態 40 | td 41 | == dp.statS 42 | tr 43 | th.nowrap 44 | | 選択中のチャンネル 45 | td 46 | == dp.selCh() 47 | tr 48 | th.nowrap 49 | | 番組名 50 | td 51 | | #{dp.prog_name} 52 | tr 53 | th.nowrap 54 | | 番組概要 55 | td 56 | | #{dp.prog_detail} 57 | tr 58 | th.nowrap 59 | | アクション 60 | td 61 | a.btn.waves-effect.waves-light.btn-small href="#{dp.base_url}/stop" class="#{dp.dis_stop()}" 停止 62 | 63 | .row 64 | div#band 65 | ul.tabs 66 | - dp.bands().each do |band| 67 | li.tab.col.s2 68 | a href="##{band}" class="#{dp.activeBand?( band )}" 69 | | #{dp.bandname[band]} 70 | 71 | div 72 | - dp.bands().each do |band| 73 | div.col.s8 id = band 74 | == dp.data[band] 75 | 76 | 77 | 78 | 79 | 80 | 81 | javascript: 82 | $(function(){ 83 | $( 'input[name="devfn"]:radio' ).change( function() { 84 | var radioval = $(this).val(); 85 | window.location.href = "/mpv_mon/" + radioval + "/disp/"; 86 | }); 87 | }); 88 | 89 | $(document).ready(function(){ 90 | $('.tabs').tabs(); 91 | }); 92 | 93 | $(document).ready(function(){ 94 | $('select').formSelect(); 95 | }); 96 | 97 | 98 | 99 | == Commlib::include( SrcDir + "/views/move_top.html" ) 100 | -------------------------------------------------------------------------------- /src/views/ch_info.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # ch 情報 6 | # 7 | 8 | 9 | class ChaneruInfo 10 | 11 | attr_reader :name, :skip, :phch, :svid, :tt 12 | 13 | def initialize( ) 14 | ( @data, @chid_prog ) = getChData( ) 15 | @older = 3600 * 24 * 31 # 1ヶ月 16 | end 17 | 18 | # 19 | # channelデータの取得 20 | # 21 | def getChData( ) 22 | ret = nil 23 | chid_prog = {} 24 | DBaccess.new().open do |db| 25 | channel = DBchannel.new 26 | ret = channel.select( db, order: "order by band_sort,chid" ) 27 | 28 | # 過去から全ての予約数 29 | reserve = DBreserve.new 30 | prog = reserve.select( db ) 31 | prog.each do |tmp| 32 | chid_prog[ tmp[:chid] ] ||= 0 33 | chid_prog[ tmp[:chid] ] += 1 34 | end 35 | end 36 | [ ret, chid_prog ] 37 | end 38 | 39 | # 40 | # 表 41 | # 42 | def printTable() 43 | ret = [] 44 | n = 1 45 | b = %Q(削除) 46 | 47 | now = Time.now.to_i 48 | @data.each do |d| 49 | t = d[:updatetime].to_i 50 | next if t == -1 51 | color = now - t > @older ? "color9" : "" 52 | date = t == 0 ? "-" : Time.at( t ).strftime("%Y-%m-%d %H:%M:%S") 53 | skip = d[:skip] == 1 ? "On" : "-" 54 | count = @chid_prog[ d[:chid] ] == nil ? 0 : @chid_prog[ d[:chid] ] 55 | #del = sprintf( b, count == 0 ? "" : "disabled" , d[:chid], ) 56 | del = sprintf( b, "", d[:chid], ) 57 | 58 | arg = [ n, d[:band], d[:chid], d[:name],d[:tsid],d[:onid], 59 | d[:svid],d[:stinfo_tp],d[:stinfo_slot], skip, date, count, del] 60 | ret << Commlib::printTR2( arg, trcl: [color] ) 61 | n += 1 62 | end 63 | ret.join() 64 | end 65 | 66 | # 67 | # EPG パッチデータ 68 | # 69 | def epgPatchTable() 70 | 71 | ret = [] 72 | if $epgPatch == nil 73 | $epgPatch = EpgPatch.new.getData() 74 | end 75 | 76 | $epgPatch.each_pair do |k,v| 77 | v.each_pair do |k2,v2| 78 | arg = [ k, k2, v2 ] 79 | ret << Commlib::printTR2( arg ) 80 | end 81 | end 82 | ret.join() 83 | 84 | end 85 | 86 | 87 | 88 | end 89 | -------------------------------------------------------------------------------- /src/views/prg_dialog.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 番組詳細 6 | # 7 | 8 | 9 | class Dialog_pid 10 | 11 | Base = "/prgtbl" 12 | 13 | def initialize( pid ) 14 | @pid = pid 15 | end 16 | 17 | def printTable2( title, val) 18 | sprintf(%Q{ %s %s },title, val) 19 | end 20 | 21 | def extdetail2table( data ) 22 | r = [] 23 | r << %Q{} 24 | data.each do |tmp| 25 | title = tmp[ "item_description" ] 26 | item = tmp[ "item" ] 27 | r << %Q{ } 28 | r << %Q{ } 29 | r << %Q{ } 30 | end 31 | r << %Q{
#{title} #{item}
} 32 | 33 | r.join("\n") 34 | end 35 | 36 | 37 | 38 | def getData() 39 | row = nil 40 | programs = DBprograms.new() 41 | category = DBcategory.new( ) 42 | 43 | DBaccess.new(DbFname).open do |db| 44 | data = programs.selectSP( db, proid: @pid ) 45 | if data.size > 0 46 | data2 = data.first 47 | cate = [] 48 | data2[:categoryA].each do |tmp| 49 | if tmp[0] != 0 50 | cate << category.conv2str(db, tmp[0],tmp[1] ).join(" : ") 51 | end 52 | end 53 | return [ data2,cate ] 54 | end 55 | end 56 | [ nil, nil ] 57 | end 58 | 59 | # 60 | # データの表示 61 | # 62 | def printTable() 63 | r = [] 64 | 65 | ( tmp, cate ) = getData() 66 | if tmp != nil and tmp.size > 0 67 | r << printTable2("放送局名", tmp[:name] + " (#{tmp[:chid]})" ) 68 | r << printTable2("番組名", tmp[:title] + " (evid=#{tmp[:evid]})" ) 69 | r << printTable2("概要", tmp[:detail] ) if tmp[:detail].strip != "" 70 | r << printTable2("録画時間", Commlib::stet_to_s( tmp[:start], tmp[:end] ).join(" ")) 71 | 72 | if cate.size > 0 73 | r << printTable2("分類", cate.join("
\n") ) 74 | end 75 | 76 | if tmp[:extdetail] != nil and tmp[:extdetail].strip != "" 77 | if ( data = YamlWrap.load( tmp[:extdetail])).size > 0 78 | r << printTable2("詳細情報", extdetail2table( data )) 79 | end 80 | end 81 | end 82 | r.join("\n") 83 | end 84 | end 85 | 86 | -------------------------------------------------------------------------------- /src/db/Phchid.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class DBphchid 6 | 7 | include Base 8 | 9 | def initialize( ) 10 | @list = { 11 | id: "id", 12 | phch: "phch", 13 | chid: "chid", 14 | updatetime: "updatetime", 15 | } 16 | @para = [] 17 | @list.each_pair { |k,v| @para << v } 18 | @tbl_name = "phchid" 19 | end 20 | 21 | # 22 | # 検索 23 | # 24 | def select( db, phch: nil, chid: nil, updatetime: nil ) 25 | sql = "select id,phch,chid,updatetime from #{@tbl_name} " 26 | where = [] 27 | args = [] 28 | if phch != nil 29 | where << " phch = ? " 30 | args << phch 31 | end 32 | if chid != nil 33 | where << " chid = ? " 34 | args << chid 35 | end 36 | if updatetime != nil 37 | where << " updatetime < ? " 38 | args << updatetime 39 | end 40 | 41 | if where.size > 0 42 | sql += " where " + where.join(" and ") 43 | end 44 | 45 | row = db.execute( sql, args ) 46 | row2hash( @list, row ) 47 | end 48 | 49 | # 50 | # 追加 51 | # 52 | def add(db, phch, chid, updatetime ) 53 | sql = "select id from #{@tbl_name} where phch = ? and chid = ? " 54 | row = db.execute( sql, phch, chid ) 55 | if row.size == 0 56 | sql = "insert into #{@tbl_name} ( phch, chid, updatetime) values (?,?,?)" 57 | db.execute( sql, phch, chid, updatetime ) 58 | else 59 | sql = "update #{@tbl_name} set updatetime = ? where phch = ? " 60 | db.execute( sql, updatetime, phch ) 61 | end 62 | end 63 | 64 | # 65 | # touch 66 | # 67 | def touch(db, updatetime, phch: nil, chid: nil ) 68 | sql = "update #{@tbl_name} set updatetime = ? " 69 | where = [] 70 | args = [ updatetime ] 71 | if phch != nil 72 | where << " phch = ? " 73 | args << phch 74 | end 75 | if chid != nil 76 | where << " chid = ? " 77 | args << chid 78 | end 79 | if where.size > 0 80 | sql += " where " + where.join(" and ") 81 | end 82 | 83 | db.execute( sql,args ) 84 | end 85 | 86 | # 87 | # 削除 88 | # 89 | def delete(db, phch ) 90 | sql = "delete from #{@tbl_name} where phch = ? " 91 | db.execute( sql, phch ) 92 | end 93 | 94 | end 95 | -------------------------------------------------------------------------------- /src/views/fil_res_dsp.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # フィルター検索結果の表示 6 | # 7 | 8 | class FilterDisp 9 | 10 | def initialize( params ) 11 | @freeOnly = params["FO"] == "on" ? true : false 12 | end 13 | 14 | def filter?() 15 | return @fa_flag == 0 ? true : false 16 | end 17 | 18 | def fo?() 19 | return @freeOnly 20 | end 21 | 22 | # 23 | # データの表示 24 | # 25 | def print( id, session ) 26 | r = {} 27 | prog = DBprograms.new 28 | filresR = DBfilterResult.new 29 | reserve = DBreserve.new 30 | filter = DBfilter.new 31 | now = Time.now.to_i 32 | DBaccess.new().open do |db| 33 | data = filter.select( db, id: id ) 34 | if data[0] != nil 35 | @fa_flag = data[0][:type] == FilConst::Filter ? 0 : 1 36 | data = filresR.select(db, pid: id ) 37 | data2 = data.map{|v| v[:rid] } 38 | data3 = prog.selectSP( db, proid: data2 ) 39 | count = 1 40 | total = data.size 41 | tmp = [] 42 | tdclas = [ "item" ] 43 | data3.each do |t| 44 | next if t[:end] < now 45 | (day, time, w) = Commlib::stet_to_s( t[:start], t[:end] ) 46 | cate = t[:categoryA][0][0] 47 | trcl = %W(color#{cate}) 48 | res = reserve.select( db, evid: t[:evid], chid: t[:chid] ) 49 | res2 = "" 50 | resid = nil 51 | if res.size > 0 52 | if @freeOnly == true 53 | total -= 1 54 | next 55 | end 56 | if res[0][:stat] == RsvConst::Normal or res[0][:stat] == RsvConst::RecNow 57 | res2 = "○" 58 | else 59 | res2 = "×" 60 | end 61 | resid = res[0][:id] 62 | end 63 | name = %Q( #{t[:name]}) 64 | data = [ count,name, day,time,res2,t[:title],t[:detail] ] 65 | tmp << Commlib::printTR2( data, rid: t[:pid], trcl: trcl, tdcl: tdclas, tdclf: 2, resid: resid ) 66 | count += 1 67 | end 68 | r[:title] = sprintf("

検索結果 %d 件

", total ) 69 | 70 | r[:table] = tmp.join("\n") 71 | end 72 | end 73 | r 74 | end 75 | 76 | end 77 | 78 | -------------------------------------------------------------------------------- /src/model/DiskKeep.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # 4 | # disk の空き容量確保 5 | # 6 | require 'find' 7 | require 'pp' 8 | require 'sys/filesystem' 9 | 10 | class DiskKeep 11 | 12 | def initialize( ) 13 | @gb = 1000 * 1000 * 1000 14 | @mb = 1000 * 1000 15 | end 16 | 17 | def start( db ) 18 | 19 | return if DiskKeepPercent == nil or DiskKeepPercent == false 20 | 21 | buf = [] 22 | keep = DiskKeepPercent.to_f / 100 23 | stat = Sys::Filesystem.stat( TSDir ) 24 | 25 | total = (stat.blocks * stat.block_size).to_f 26 | free = (stat.blocks_available * stat.block_size).to_f 27 | target = total * keep 28 | delsize = ( total * keep ) - free 29 | 30 | #printf("Disk容量 : %6.2f GB\n", total / @gb ) 31 | #printf("確保目標 : %6.2f GB (%.1f%%)\n", target /@gb, keep * 100 ) 32 | #printf("空き容量 : %6.2f GB (%.1f%%)\n", free / @gb, 100.0 * free / total ) 33 | 34 | buf << sprintf("削除量 = %6.2f GB", delsize > 0 ? delsize / @gb : 0 ) 35 | 36 | if free < ( total * keep ) 37 | list = {} 38 | Find.find( TSDir ) do |path| 39 | if test( ?f, path ) 40 | if path =~ /\.ts$/ 41 | fs = File.stat( path ) 42 | list[ fs.mtime ] ||= [] 43 | list[ fs.mtime ] << path 44 | end 45 | end 46 | end 47 | 48 | t = 0 49 | catch(:break_loop) do 50 | list.keys.sort.each do |mtime| 51 | list[ mtime ].each do |path| 52 | fs = File.stat( path ).size 53 | t += fs 54 | buf << sprintf("ファイル削除 %s (%5.1f GB)", File.basename(path),fs / @gb ) 55 | File.unlink( path ) 56 | path += ".chk" # packect chk result 57 | File.unlink( path ) if test( ?f, path ) 58 | 59 | if t > delsize 60 | buf << sprintf("削除合計 = %6.1f GB", t/ @gb ) 61 | throw :break_loop 62 | end 63 | end 64 | end 65 | end 66 | end 67 | 68 | # 69 | # 空のディレクトリを削除 70 | # 71 | Dir.entries( TSDir ).sort.each do |dir1| 72 | if dir1 != '..' && dir1 != '.' 73 | path = TSDir + "/" + dir1 74 | if test( ?d, path ) 75 | if Dir.entries( path ).size == 2 76 | buf << sprintf("rmdir %s",path ) 77 | Dir.rmdir( path ) 78 | end 79 | end 80 | end 81 | end 82 | 83 | buf.each do |tmp| 84 | DBlog::debug( db,"DiskKeep: " + tmp ) 85 | end 86 | end 87 | 88 | 89 | end 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/lib/setConst.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'lib/deviceChk.rb' 4 | 5 | # 6 | # config の設定 7 | # 8 | files = [ ] 9 | files << ENV["RASPIREC_CONF_OPT"] if ENV["RASPIREC_CONF_OPT"] != nil 10 | files << ENV["RASPIREC_CONF"] if ENV["RASPIREC_CONF"] != nil 11 | files << ENV["HOME"] + "/.config/raspirec/config.rb" 12 | files.each do |cfg| 13 | if test( ?f, cfg ) 14 | require cfg 15 | ConfigPath = cfg 16 | break 17 | end 18 | end 19 | raise "config not found" if Object.const_defined?(:BaseDir) != true 20 | 21 | 22 | # 23 | # 後から追加した定数のデフォルト設定 24 | # 25 | 26 | if Object.const_defined?(:TitleRegex) != true 27 | tmp = [ # 題名の削除フィルターが未定義の場合のデフォルト 28 | /【N】/, 29 | /【SS】/, 30 | /【デ】/, 31 | /【再】/, 32 | /【双】/, 33 | /【多】/, 34 | /【天】/, 35 | /【字】/, 36 | /【新】/, 37 | /【無】/, 38 | /【解】/, 39 | /【終】/, 40 | /【初】/, 41 | ] 42 | Object.const_set("TitleRegex",tmp ) 43 | end 44 | 45 | if Object.const_defined?(:SearchStringRegex) != true 46 | tmp = [ # 検索文字列の削除フィルターが未定義の場合のデフォルト 47 | /\#\d+\s?[・-]\s?\#\d+/, 48 | /[\#♯#][1234567890\d]+/, 49 | /第[一二三四五六七八九十1234567890\d]+話/, 50 | /「.*」/, 51 | ] + TitleRegex 52 | Object.const_set("SearchStringRegex",tmp ) 53 | end 54 | 55 | 56 | # 57 | # 定数が設定されていない場合にデフォルト値を設定 58 | # 59 | def setDefaultConst( name, val ) 60 | sym = name.to_sym 61 | if Object.const_defined?( sym ) != true 62 | Object.const_set(sym, val ) 63 | end 64 | end 65 | 66 | setDefaultConst("MPMonitor",false ) 67 | setDefaultConst("GBC_tuner_num", 0 ) 68 | setDefaultConst("DeviceList_GBC",[]) 69 | setDefaultConst("EpgBanTime", nil ) 70 | setDefaultConst("DeviceChkFN", DBDir + "/devicechk.yaml" ) 71 | setDefaultConst("EPG_tuner_limit",false ) 72 | setDefaultConst("Browser_cmd", "/usr/bin/firefox" ) 73 | setDefaultConst("AutoRecExt", false ) 74 | setDefaultConst("ARE_sampling_time", 90 ) 75 | setDefaultConst("ARE_epgdump_opt", %w( --tail 50M ) ) 76 | setDefaultConst("RaspirecTV_GAPT", 1 ) 77 | 78 | 79 | # 80 | # パケットチェック機能を有効にするか? 81 | # 82 | if ( Object.const_defined?(:PacketChk_enable) == true ) && 83 | PacketChk_enable == true 84 | if Object.const_defined?(:PacketChk_cmd) == true && 85 | test( ?f, PacketChk_cmd ) 86 | Object.const_set("PacketChkRun", true ) 87 | else 88 | tmp = sprintf("PacketChk_cmd not found %s : PacketChk_enable -> false\n",PacketChk_cmd) 89 | DBlog::warn( nil,tmp) 90 | Object.const_set("PacketChkRun", false ) 91 | end 92 | else 93 | Object.const_set("PacketChkRun", false ) 94 | end 95 | 96 | -------------------------------------------------------------------------------- /src/views/test.slim: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title = @title 4 | meta http-equiv='Content-Type' content='text/html' charset='utf-8' 5 | link rel='stylesheet' href='/materialize/css/materialize.css' media='screen,projection' 6 | meta name= 'viewport' content='width=device-width, initial-scale=1.0' 7 | 8 | body 9 | script src="//code.jquery.com/jquery-1.11.3.min.js" 10 | script src="//code.jquery.com/jquery-migrate-1.2.1.min.js" 11 | 12 | css: 13 | .line{ 14 | position: absolute; 15 | border-top: solid 1px #FF4981; 16 | -moz-transform-origin:0% 0%; 17 | -webkit-transform-origin:0% 0%; 18 | transform-origin:0% 0%; 19 | float: left; 20 | } 21 | 22 | javascript: 23 | function DrawLine(params) { 24 | var param = jQuery.extend({ 25 | ID: 0 26 | , x1: 0 27 | , y1: 0 28 | , x2: 0 29 | , y2: 0 30 | , line_style: "solid" 31 | , line_color: "#000" 32 | , line_width: "1px" 33 | , parent: $("body") 34 | , callback: function(){} 35 | }, params); 36 | if(param.x1 < param.x2){ 37 | sx = param.x1; 38 | sy = param.y1; 39 | ex = param.x2; 40 | ey = param.y2; 41 | } 42 | else { 43 | sx = param.x2; 44 | sy = param.y2; 45 | ex = param.x1; 46 | ey = param.y1; 47 | } 48 | var w = Math.sqrt(Math.pow((sx - ex) ,2) + Math.pow((sy - ey) ,2)); 49 | var base = Math.max(sx, ex) - Math.min(sx, ex); 50 | var tall = Math.max(sy, ey) - Math.min(sy, ey); 51 | var aTan = Math.atan(tall / base); 52 | var deg = aTan * 180 / Math.PI; 53 | deg = sy > ey ? 0 - deg: deg; 54 | var line = $("
") 55 | .addClass("line") 56 | .css({ 57 | "left": sx, 58 | "top": sy, 59 | "width": w, 60 | "transform": "rotate(" + deg + "deg)", 61 | "-webkit-transform": "rotate(" + deg + "deg)", 62 | "border-top-style": param.line_style, 63 | "border-top-color": param.line_color, 64 | "border-top-width": param.line_width, 65 | }); 66 | $(param.parent).append(line); 67 | } 68 | 69 | $(document).ready(function() 70 | { 71 | DrawLine({ 72 | ID: "L2SW11P0-L2SW12P0", 73 | x1: 0, 74 | y1: 0, 75 | x2: 100, 76 | y2: 100, 77 | }); 78 | DrawLine({ 79 | ID: "L2SW11P1-L2SW12P1", 80 | x1: 100, 81 | y1: 100, 82 | x2: 200, 83 | y2: 0, 84 | }); 85 | for(var i=0; i<=10; i++){ 86 | $('#L2SW11P1-L2SW12P1').css({"left": 100 - i*10}); 87 | alert(); 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /src/views/ch_info.slim: -------------------------------------------------------------------------------- 1 | / 2 | / チャンネル情報の表示 3 | / 4 | 5 | css: 6 | td,th { 7 | padding: 5px ; 8 | white-space: nowrap; 9 | } 10 | span { color: #000000} 11 | td.ex_border { 12 | border: 2px dotted black; 13 | } 14 | 15 | - require_relative 'ch_info.rb' 16 | - dp = ChaneruInfo.new( ) 17 | - eap = EpgAutoPatch.new() 18 | 19 | 20 | .row 21 | .col.s6 22 | .card.tbl 23 | .card-content 24 | table 25 | tr 26 | td chid 27 | td チャンネルID 28 | tr 29 | td name 30 | td 放送局名 31 | tr 32 | td tsid 33 | td トランスポートストリームID ( transport_stream_id ) 34 | tr 35 | td onid 36 | td オリジナルネットワークID ( original_network_id ) 37 | tr 38 | td svid 39 | td サービスID ( service_id ) 40 | tr 41 | td stinfo_tp 42 | td 衛星の場合はトランスポート番号、地デジの場合は物理チャンネル番号 43 | tr 44 | td stinfo_slot 45 | td 衛星の場合はスロット番号 46 | tr 47 | td skip 48 | td 放送局一覧で、「番組表、検索の対象から外す。」が設定されているか 49 | tr 50 | td 予約数 51 | td 予約テーブル上にある全て(過去から未来)の予約数。 52 | tr 53 | td 更新日時 54 | td EPGの更新日時で、1ヶ月以上更新が無い場合は背景色が変わる。 55 | 56 | 57 | 58 | div 59 | table.striped 60 | tr 61 | th No 62 | th バンド 63 | th chid 64 | th name 65 | th tsid 66 | th onid 67 | th svid 68 | th stinfo_tp 69 | th stinfo_slot 70 | th skip 71 | th 更新日時 72 | th 予約数 73 | th 削除 74 | == dp.printTable() 75 | 76 | p style="padding-top:3em" 77 | 78 | a name="BSSLOT" 79 | 80 | .row 81 | .col.s10 82 | .card.tbl 83 | .card-content 84 | span#title.card-title BSデジタル放送のチャンネル割当 85 | table 86 | == eap.printBschHtml() 87 | 88 | .row 89 | .col.s6 90 | .card.tbl 91 | .card-content 92 | span#title.card-title BS チャンネルの自動補正 93 | table 94 | tr 95 | th 局名 96 | th 変更前 97 | th 変更後 98 | == eap.printConvHtml() 99 | 100 | 101 | 102 | 103 | javascript: 104 | $(".btn").click(function(event){ 105 | var chid = $(this).attr('chid'); 106 | event.preventDefault(); 107 | //console.log( chid ); 108 | $.ajax({ 109 | url: '/ch_info/del/' + chid, 110 | type: "POST", 111 | data: '&chid=1', 112 | async: true, 113 | timeout: 30000 114 | }).done( function(results) { 115 | location.reload(); 116 | }) 117 | }); 118 | 119 | 120 | 121 | == Commlib::include( SrcDir + "/views/move_top.html" ) 122 | -------------------------------------------------------------------------------- /src/model/Daily.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | # 6 | # 一日一回 7 | # 8 | 9 | class Daily 10 | 11 | @@lastTime = nil # 前回の実行時間 12 | 13 | def initialize( ) 14 | end 15 | 16 | def run() 17 | keyval = DBkeyval.new 18 | key = "daily" 19 | 20 | if @@lastTime == nil 21 | DBaccess.new().open do |db| 22 | @@lastTime = keyval.select( db, key ) 23 | if @@lastTime == nil 24 | @@lastTime = Time.now.to_i 25 | keyval.upsert(db, key, @@lastTime ) 26 | return 27 | end 28 | end 29 | end 30 | 31 | threshold = (Time.now - ( 24 * 3600 )).to_i 32 | if @@lastTime < threshold 33 | 34 | # 35 | # 二重録画のチェック(デバック時のみ) 36 | # 37 | if $debug == true 38 | DBlog::sto("++++ 二重録画のチェック ++++") 39 | DBaccess.new().open( ) do |db| 40 | reserve = DBreserve.new 41 | reserve.dupRecChk( db ) 42 | end 43 | end 44 | 45 | # 46 | # 古いデータの削除 47 | # 48 | DBaccess.new().open( tran: true ) do |db| 49 | DBlog::debug( db, "daily task start" ) 50 | now = Time.now.to_i 51 | DBlog.new.deleteOld( db, now - LogSaveDay * 24 * 3600 ) 52 | DBreserve.new.deleteOld( db, now - RsvHisSaveDay * 24 * 3600 ) 53 | 54 | lr = LogRote.new() 55 | if lr.need?() == true 56 | DBlog::debug( db, "Log rotate" ) 57 | sleep(1) 58 | lr.exec() 59 | end 60 | 61 | @@lastTime = Time.now.to_i 62 | keyval.upsert(db, key, @@lastTime ) # 日付更新 63 | end 64 | 65 | # 66 | # DB ファイルのバックアップ 67 | # 68 | sleep(1) 69 | DBlog::vacuum() 70 | sleep(1) 71 | wday = Time.now.strftime("%a") 72 | fname = sprintf( "%s.backup-%s", DbFname ,wday ) 73 | FileUtils.cp( DbFname, fname ) 74 | DBlog::debug( nil, "DB file backup (#{fname})" ) 75 | 76 | # 77 | # json ファイルの掃除 78 | # 79 | older = Time.now - ( ( EPGperiod + 1 ) * 3600 ) 80 | Dir.open( JsonDir ).each do |file| 81 | next if file == "." or file == ".." 82 | if file =~ /\.(json|tmp)$/ 83 | path = JsonDir + "/" + file 84 | if test( ?f, path ) 85 | mtime = File.mtime( path ) 86 | if mtime < older 87 | File.unlink( path ) 88 | end 89 | end 90 | end 91 | end 92 | end 93 | end 94 | end 95 | 96 | 97 | 98 | if File.basename($0) == "Daily.rb" 99 | base = File.dirname( $0 ) 100 | [ ".", "..","src", base ].each do |dir| 101 | if test( ?f, dir + "/require.rb") 102 | $: << dir 103 | end 104 | end 105 | require 'require.rb' 106 | 107 | Daily.new.run 108 | 109 | end 110 | 111 | -------------------------------------------------------------------------------- /src/views/tooltip.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 63 | 64 | 103 | -------------------------------------------------------------------------------- /src/lib/misc.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | 4 | # 5 | # 配管工事 6 | # 7 | def reopenSTD( sto, ste ) 8 | 9 | old_out = $stdout.dup 10 | old_err = $stderr.dup 11 | $stdin.reopen("/dev/null") 12 | if $debug == true 13 | $stdout.reopen( sto, "a") 14 | $stderr.reopen( ste, "a") 15 | if false 16 | $stdout.sync = true 17 | $stderr.sync = true 18 | else 19 | Thread.new do 20 | while true 21 | $stdout.flush 22 | $stderr.flush 23 | sleep(1) 24 | end 25 | end 26 | end 27 | else 28 | $stdout.reopen("/dev/null", "w") 29 | $stderr.reopen("/dev/null", "w") 30 | end 31 | 32 | old_out.close 33 | old_err.close 34 | #[ old_out, old_err ] 35 | end 36 | 37 | 38 | 39 | # 40 | # trap の設置 41 | # 42 | def setTrap() 43 | 44 | name = File.basename( $0 ) 45 | Signal.trap( :HUP ) { DBlog::sto("#{name} :HUP") ; endParoc() } 46 | Signal.trap( :INT ) { DBlog::sto("#{name} :INT") ; endParoc() } 47 | Signal.trap( :QUIT ) { DBlog::sto("#{name} :QUIT") } 48 | Signal.trap( :SYS ) { DBlog::sto("#{name} :SYS") } 49 | Signal.trap( :TERM ) { DBlog::sto("#{name} :TERM") ; endParoc() } 50 | #Signal.trap( :CHLD ) { DBlog::sto("#{name} :CHLD") } 51 | Signal.trap( :EXIT) { 52 | DBlog::sto( name + " " + $! ) 53 | pp $! 54 | } 55 | 56 | if name == "timer_main.rb" 57 | Signal.trap( :CHLD ) { 58 | #DBlog::sto("#{name} :CHLD"); 59 | childWait() 60 | } 61 | end 62 | 63 | end 64 | 65 | 66 | def tailLog( out = STDOUT ) 67 | out.puts("tailLog()") 68 | bsize = 1024 * 1024 69 | outbuf = "x" * bsize 70 | 71 | fps = [] 72 | [ StdoutH, StdoutT, StderrH, StderrT ].each do |fn| 73 | if test( ?f, fn ) 74 | fp = open( fn ) 75 | fp.sysseek(0, IO::SEEK_END) 76 | fps << fp 77 | end 78 | end 79 | 80 | count = 0 81 | while true 82 | if ( r = IO.select( fps, nil, nil,1 )) != nil 83 | r[0].each do |fp| 84 | if fp != nil 85 | fp.read( bsize, outbuf ) 86 | if outbuf != nil 87 | outbuf.force_encoding("ASCII-8bit") 88 | outbuf.each_line do |l| 89 | next if l =~ /\/var\/lib\// 90 | next if l =~ /from \/usr\/lib\// 91 | next if l =~ /style\.css/ 92 | next if l =~ /overlaid\.css/ 93 | next if l =~ /^http:/ 94 | next if l =~ /127\.0\.0/ 95 | next if l =~ /192\.168\.1/ 96 | next if l =~ /\/usr\/lib\/ruby/ 97 | out.write( l ) 98 | end 99 | end 100 | end 101 | end 102 | end 103 | sleep(0.1) 104 | end 105 | end 106 | 107 | 108 | 109 | def ppp( str ) 110 | if $debug == true 111 | if @ts != nil 112 | DBlog::sto( str + " " + (Time.now - @ts).to_s ) 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /src/views/log_view.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | class LogView 4 | 5 | Base = "/log_view" 6 | 7 | def initialize( level, page ) 8 | @radio = { :debug => false, 9 | :info => false, 10 | :atte => false, 11 | :warn => false, 12 | :err => false, 13 | } 14 | if level != nil 15 | case level.to_i 16 | when DBlog::Debug then @radio[:debug] = true 17 | when DBlog::Info then @radio[:info] = true 18 | when DBlog::Attention then @radio[:atte] = true 19 | when DBlog::Warning then @radio[:warn] = true 20 | when DBlog::Error then @radio[:err] = true 21 | end 22 | @level = level.to_i 23 | else 24 | @level = 2 25 | @radio[:info] = true 26 | end 27 | 28 | if page == nil 29 | @page = 1 30 | else 31 | @page = page.to_i 32 | end 33 | @page_line = 256 34 | 35 | @data = getData() 36 | end 37 | 38 | # 39 | # pageのセレクト 40 | # 41 | def pageSel( ) 42 | r = [] 43 | r << %Q{} 51 | 52 | r.join("\n") 53 | end 54 | 55 | def getData() 56 | log = DBlog.new 57 | ret = [] 58 | DBaccess.new().open do |db| 59 | @total_size = log.count( db, level: @level - 1) 60 | @pageNum = 1 61 | if @total_size > @page_line 62 | @pageNum = @total_size / @page_line 63 | if @pageNum > 0 64 | @pageNum += 1 if (@total_size - ( @page_line * @pageNum )) > 0 65 | end 66 | end 67 | limit = nil 68 | if @pageNum > 1 69 | limit = "LIMIT #{@page_line} OFFSET #{@page_line * (@page - 1 )}" 70 | end 71 | ret = log.select( db, level: @level - 1, limit: limit ) 72 | end 73 | ret 74 | end 75 | 76 | def printTable() 77 | log = DBlog.new 78 | ret = [] 79 | @data.each do |r| 80 | r.shift 81 | if r[0] >= @level 82 | tdcl = %w( nowrap ) 83 | r[0] = case r[0] 84 | when DBlog::Debug then "デバッグ" 85 | when DBlog::Info then "情報" 86 | when DBlog::Attention then tdcl << "atte" ; "注意" 87 | when DBlog::Warning then tdcl << "warn" ; "警告" 88 | when DBlog::Error then tdcl << "error" ; "エラー" 89 | end 90 | r[1] = Time.at( r[1] ).strftime("%Y-%m-%d %H:%M:%S") 91 | ret << Commlib::printTR2( r, tdcl: tdcl ) 92 | end 93 | end 94 | ret.join("\n") 95 | end 96 | 97 | def radio() 98 | @radio 99 | end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /src/views/rsv_list.slim: -------------------------------------------------------------------------------- 1 | / 2 | / 予約一覧 3 | / 4 | 5 | css: 6 | td,th { 7 | padding: 5px ; 8 | } 9 | span { color: #000000} 10 | 11 | 12 | - require_relative 'rsv_list.rb' 13 | - dp = ReservationList.new( ) 14 | - data = dp.getData() 15 | 16 | div 17 | table.striped 18 | tr 19 | th No 20 | th 放送局 21 | th 日付 22 | th 時間 23 | th 状態 24 | th 種別 25 | th タイトル 26 | == dp.printTable() 27 | 28 | 29 | 30 | div id="sample-dialog" title="詳細" style="display:none;" 31 | p 32 | 33 | 34 | javascript: 35 | $(".dialog").click(function(event){ 36 | var rid = $(this).attr('rid'); 37 | var recf = $(this).attr('recf'); 38 | //event.preventDefault(); 39 | $("#sample-dialog").load('/rsv_list_D/' + rid ); 40 | $("#sample-dialog").dialog({ 41 | modal: true, 42 | maxWidth: 1200, 43 | width: 1000, 44 | buttons: { //ボタン 45 | "修正": function() { 46 | $.ajax({ 47 | url: '/rsv_list/Mod/' + rid, 48 | type:"POST", 49 | data: $('form').serialize() + '¶m=1', 50 | async: true, 51 | timeout: 30000 52 | }).done( function(results) { 53 | location.reload(); 54 | }) 55 | }, 56 | "削除": function() { 57 | $.ajax({ 58 | url: '/rsv_list/Del/' + rid, 59 | type:"POST", 60 | async: true, 61 | timeout: 30000 62 | }).done( function(results) { 63 | location.reload(); 64 | }) 65 | }, 66 | "録画中止": function() { 67 | $.ajax({ 68 | url: '/rsv_list/Stop/' + rid, 69 | type:"POST", 70 | async: true, 71 | timeout: 30000 72 | }).done( function(results) { 73 | location.reload(); 74 | }) 75 | }, 76 | "閉じる": function() { 77 | $(this).dialog("close"); 78 | } 79 | }, 80 | open: function() { 81 | $(this).siblings('.ui-dialog-buttonpane').find('button:eq(3)').focus(); 82 | if ( recf == 0 ) { 83 | $(".ui-dialog-buttonpane button:contains('録画中止')").button('disable'); 84 | } else { 85 | $(".ui-dialog-buttonpane button:contains('修正')").button('disable'); 86 | $(".ui-dialog-buttonpane button:contains('削除')").button('disable'); 87 | } 88 | } 89 | }); 90 | return false; 91 | }); 92 | 93 | == Commlib::include( SrcDir + "/views/move_top.html" ) 94 | -------------------------------------------------------------------------------- /src/model/Control.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 予約一覧 6 | # 7 | require 'sys/filesystem' 8 | 9 | class Control 10 | 11 | def initialize( ) 12 | 13 | end 14 | 15 | # 16 | # ログファイルのローテート 17 | # 18 | def logrote() 19 | lr = LogRote.new() 20 | DBlog::sto( "Log rotate" ) 21 | lr.exec() 22 | end 23 | 24 | # 25 | # file 転送 26 | # 27 | def fcopy( params ) 28 | Thread.new do 29 | fname = params[ "fname" ] 30 | from = TSDir + "/" + fname 31 | return unless test( ?f, from ) 32 | 33 | subdir = File.dirname( fname ) 34 | errmsg = nil 35 | fc = FileCopy.new 36 | ( speed, errmsg ) = fc.scp( TSFT_toDir, subdir, from ) 37 | 38 | if errmsg == nil 39 | tmp = sprintf("手動転送終了: %s (%.1f Mbyte/sec)", fname, speed ) 40 | else 41 | tmp = sprintf("手動転送失敗: %s : %s",errmsg , fname ) 42 | end 43 | DBaccess.new().open() do |db| 44 | DBlog::info(db,tmp) 45 | end 46 | end 47 | end 48 | 49 | def tsft( arg ) # "true" で不許可 50 | DBaccess.new().open( tran: true ) do |db| 51 | DBkeyval.new.upsert( db, "tsft", arg ) 52 | if arg != "true" 53 | DBupdateChk.new.touch() 54 | end 55 | end 56 | end 57 | 58 | def logdel( arg ) 59 | DBaccess.new().open( tran: true ) do |db| 60 | sql = "delete from log " 61 | if arg != "all" 62 | n = arg.to_i 63 | if n > 0 64 | time = Time.now.to_i - n * 3600 * 24 65 | sql += " where time < #{time} ;" 66 | end 67 | end 68 | db.execute( sql ) 69 | end 70 | end 71 | 72 | def epg() 73 | phchid = DBphchid.new 74 | t = Time.now.to_i - ( 3600 * 24 ) 75 | DBaccess.new().open( tran: true ) do |db| 76 | row = phchid.select( db ) 77 | row.each do |r| 78 | phchid.touch( db, t, phch: r[:phch] ) 79 | end 80 | end 81 | DBupdateChk.new.touch() 82 | end 83 | 84 | def filupd( ) 85 | FilterM.new.update() 86 | end 87 | 88 | def sendSignal( fname, signal = :HUP ) 89 | if test( ?f, fname ) 90 | File.open( fname,"r" ) do |fp| 91 | pid = fp.gets().to_i 92 | if pid > 0 93 | begin 94 | Process.kill( signal, pid ) 95 | rescue Errno::ESRCH 96 | end 97 | end 98 | end 99 | end 100 | end 101 | 102 | def restart( ) 103 | [ TimerPidFile, HttpdPidFile ].each do |fname| 104 | sendSignal( fname, :HUP ) 105 | end 106 | end 107 | 108 | def stop( ) 109 | if test( ?f, PidFile ) 110 | File.open( PidFile,"r" ) do |fp| 111 | pid = fp.gets().to_i 112 | if pid > 0 113 | begin 114 | Process.kill( :TERM, pid ) 115 | rescue Errno::ESRCH 116 | end 117 | end 118 | end 119 | end 120 | end 121 | 122 | end 123 | 124 | -------------------------------------------------------------------------------- /src/views/rsv_list_old.slim: -------------------------------------------------------------------------------- 1 | / 2 | / 予約一覧 3 | / 4 | 5 | css: 6 | td,th { 7 | padding: 5px ; 8 | } 9 | .inline-block { 10 | display: inline-block; /* インラインブロック要素にする */ 11 | padding: 0px; 12 | margin: 0px 2px 0px 2px; 13 | } 14 | .pagination { 15 | margin-left: 3em; 16 | } 17 | 18 | - require_relative 'rsv_list_old.rb' 19 | - dp = ReservationListOld.new( @params, @page ) 20 | - data = dp.getData() 21 | 22 | form id="form" method="POST" action="/rsv_list_old" 23 | .inline 24 | | 検索文字列 25 | input#email_inline( type="text" name="search" class="validate" value="" autocomplete="off" ) 26 | input( type="submit" value="検索実行" ) 27 | == dp.pageSel() 28 | 29 | div 30 | table.striped 31 | tr 32 | th No 33 | th 放送局 34 | th 日付 35 | th 時間 36 | th 状態 37 | - if TSFT == true 38 | th 転送 39 | - if PacketChkRun == true 40 | th.center 検査 41 | th 種別 42 | th タイトル 43 | == dp.printTable() 44 | 45 | 46 | div id="sample-dialog" title="詳細" style="display:none;" 47 | p データを削除しますか? 48 | 49 | div id="packetchk" title="packet chk result" style="display:none;" 50 | p 51 | 52 | == Commlib::include( SrcDir + "/views/move_top.html" ) 53 | 54 | javascript: 55 | $(".dialog").click(function(event){ 56 | var rid = $(this).attr('rid'); 57 | $("#sample-dialog").dialog({ 58 | modal: true, 59 | maxWidth: 1200, 60 | width: 1000, 61 | buttons: { //ボタン 62 | "削除": function() { 63 | $.ajax({ 64 | url: '/rsv_list/Del/' + rid, 65 | type:"POST", 66 | async: true, 67 | timeout: 30000 68 | }).done( function(results) { 69 | location.reload(); 70 | }) 71 | }, 72 | "キャンセル": function() { 73 | $(this).dialog("close"); 74 | } 75 | }, 76 | open: function() { 77 | $( this ).siblings('.ui-dialog-buttonpane').find('button:eq(2)').focus(); 78 | } 79 | }); 80 | return false; 81 | }); 82 | 83 | $(".packchk").click(function(event){ 84 | var rid = $(this).attr('rid'); 85 | //event.preventDefault(); 86 | $("#packetchk").load('/pack_chk_view/' + rid ); 87 | $("#packetchk").dialog({ 88 | modal: true, 89 | maxWidth: 1200, 90 | width: 1000, 91 | buttons: { //ボタン 92 | "閉じる": function() { 93 | $(this).dialog("close"); 94 | } 95 | }, 96 | open: function() { 97 | $( this ).siblings('.ui-dialog-buttonpane').find('button:eq(2)').focus(); 98 | } 99 | }); 100 | return false; 101 | }); 102 | 103 | -------------------------------------------------------------------------------- /src/views/mpv_mon.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # モニター 6 | # 7 | require_relative 'monitor.rb' 8 | 9 | class MpvMon < Monitor 10 | 11 | attr_reader :statS, :devfn, :base_url 12 | 13 | @@ta = nil 14 | 15 | def initialize( num, cmd ) 16 | 17 | super() 18 | @ta = $tunerArray 19 | @currenTune = nil 20 | @devfn = nil 21 | @tunNum = nil 22 | 23 | if num == nil 24 | @tunNum = @ta.first.serial 25 | else 26 | @tunNum = num.to_i 27 | end 28 | 29 | @ta.each do |t1| 30 | if t1.serial == @tunNum 31 | @currenTune = t1 # 現在選択中のチューナー 32 | @devfn = @currenTune.devfn 33 | break 34 | end 35 | end 36 | 37 | @cmd = cmd == nil ? "disp" : cmd 38 | @ta.chkDeviceStat() 39 | if @currenTune != nil 40 | @statS = @currenTune.getStatStr() 41 | @base_url = "/mpv_mon/#{@tunNum}" 42 | else 43 | @base_url = "/mpv_mon" 44 | end 45 | 46 | end 47 | 48 | 49 | # 50 | # device 選択 51 | # 52 | def deviceSelect() 53 | hina = %q( ) 58 | 59 | a = [] 60 | @ta.each do |t1| 61 | if t1.devfn != nil 62 | dev = t1.devfn 63 | sel = t1.serial == @tunNum ? "checked" : "" 64 | name = t1.name 65 | stat = t1.stat == :OK ? "" : "busy" 66 | a << sprintf( hina, t1.serial, sel, stat, name ) 67 | end 68 | end 69 | a.join("\n") 70 | end 71 | 72 | # 73 | # 選局中のチャンネル 74 | # 75 | def selCh() 76 | return "-" if @devfn == nil or @currenTune.chName == nil 77 | return @currenTune.chName 78 | end 79 | 80 | # 81 | # 選局中の番組名 82 | # 83 | def prog_name() 84 | return "-" if @devfn == nil or @currenTune.prog_name == nil 85 | return @currenTune.prog_name 86 | end 87 | 88 | # 89 | # 選局中の番組概要 90 | # 91 | def prog_detail() 92 | return "-" if @devfn == nil or @currenTune.prog_detail == nil 93 | return @currenTune.prog_detail 94 | end 95 | 96 | # 97 | # 有効なバンドを返す。 98 | # 99 | def bands() 100 | r = [] 101 | if @currenTune != nil 102 | r << "GR" if @currenTune.band[ Const::GR ] == true 103 | r << "BS" if @currenTune.band[ Const::BSCS ] == true 104 | r << "CS" if @currenTune.band[ Const::BSCS ] == true 105 | end 106 | return r 107 | end 108 | 109 | def activeBand?( band ) 110 | if @currenTune != nil 111 | return @currenTune.band[ band ] == true ? "active" : "" 112 | end 113 | return "" 114 | end 115 | 116 | def stop_a() 117 | " 停止 " 118 | end 119 | 120 | 121 | def dis_stop( ) 122 | if @currenTune != nil 123 | return @currenTune.rec_pid == nil ? "disabled" : "" 124 | end 125 | return "" 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /src/views/control.slim: -------------------------------------------------------------------------------- 1 | 2 | css: 3 | .tbl { 4 | position: absolute; 5 | width: 800px; 6 | height: 500px; 7 | margin-left: 1em; 8 | margin-top: 1em; 9 | } 10 | .filesel { 11 | width: 800px; 12 | } 13 | .btn, .switch { 14 | margin-left: 3em; 15 | } 16 | label { color: #000000} 17 | 18 | - require_relative 'control.rb' 19 | - dp = Control.new.getData() 20 | 21 | span#title.card-title コントロール パネル 22 | 23 | form( id="form" method="post" action="/control/fcopy" ) 24 | table.tbl 25 | tr 26 | td.nowrap EPGの取得 (録画中は保留されます。) 27 | td.nowrap colspan="2" 28 | a.btn.waves-effect.waves-light.green.darken-4 href="/control/epg" 実行 29 | tr 30 | td.nowrap フィルター、自動予約の再構成 31 | td.nowrap colspan="2" 32 | a.btn.waves-effect.waves-light.green.darken-4 href="/control/filupd" 実行 33 | - if TSFT == true 34 | tr 35 | td.nowrap 自動 TSファイル転送 36 | td.nowrap colspan="2" 37 | div.switch 38 | label 39 | | 許可 40 | input#tsft type="checkbox" checked=(dp[:tsft]) 41 | span.lever 42 | | 不許可 43 | tr 44 | td.nowrap 手動 TSファイル転送 45 | td.nowrap 46 | div.input-field.filesel 47 | select.filesel( name="fname" ) 48 | option( value="" disabled selected ) 49 | | ファイルを選択して下さい 50 | == dp[:tsfile] 51 | td.nowrap 52 | input type="submit" value="送信する" name="fcopy" 53 | 54 | tr 55 | td.nowrap ログ(DB)の削除 56 | td.nowrap colspan="2" 57 | a.btn.waves-effect.waves-light.green.darken-4 href="/control/logdel/all" 実行 58 | tr 59 | td.nowrap ログファイルの強制ローテート 60 | td.nowrap colspan="2" 61 | a.btn.waves-effect.waves-light.green.darken-4 href="/control/logRote" 実行 62 | tr 63 | td.nowrap プログラムの再起動 64 | td.nowrap colspan="2" 65 | a.btn.waves-effect.waves-light.green.darken-4 href="/control/restart" 実行 66 | tr 67 | td.nowrap プログラムの停止 68 | td.nowrap colspan="2" 69 | a.btn.waves-effect.waves-light.green.darken-4 href="/control/stop" 実行 70 | tr 71 | td.nowrap チャンネル情報 72 | td.nowrap colspan="2" 73 | a.btn.waves-effect.waves-light.green.darken-4 href="/ch_info" 表示 74 | 75 | tr 76 | td.nowrap config 情報 77 | td.nowrap colspan="2" 78 | a.btn.waves-effect.waves-light.green.darken-4 href="/config" 表示 79 | 80 | tr 81 | td.nowrap 補足説明 82 | td.nowrap colspan="2" 83 | a.btn.waves-effect.waves-light.green.darken-4 href="/help" 表示 84 | 85 | 86 | javascript: 87 | $('#tsft').change(function() { 88 | $('#tsft').each(function() { 89 | var r = $(this).prop('checked'); 90 | //console.log(r); 91 | $.ajax({ 92 | url: '/control/tsft/' + r, 93 | type:"get", 94 | async: false, 95 | }) 96 | }) 97 | }) 98 | 99 | $(document).ready(function(){ 100 | $('select').formSelect(); 101 | }); 102 | -------------------------------------------------------------------------------- /src/db/DB.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | require 'sqlite3' 5 | 6 | class DBaccess 7 | 8 | attr_reader :db 9 | 10 | def initialize( dbFname = DbFname ) 11 | @db = nil 12 | @DBfile = dbFname 13 | unless File.exist?(@DBfile ) 14 | open( tran: true ) do |db| 15 | createDB() 16 | end 17 | File.chmod( 0600, @DBfile ) 18 | end 19 | end 20 | 21 | def parse_caller(at) 22 | if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at 23 | file = File.basename( $1 ) 24 | line = $2.to_i 25 | method = $3 26 | return sprintf( "DB_caller : %s %d %s", file, line, method ) 27 | end 28 | "" 29 | end 30 | 31 | # 32 | # DB open mode = :immediate or :deferred or :exclusive 33 | # 34 | def open( tran: false, mode: :immediate ) # tran = true transaction 35 | #DBlog::stoD( parse_caller( caller.first ) ) if $debug == true 36 | @db = SQLite3::Database.new( @DBfile ) 37 | @db.busy_timeout(1000) 38 | ecount = 0 39 | roll = false 40 | begin 41 | roll = false 42 | if tran == true 43 | @db.transaction( mode ) do 44 | roll = true 45 | yield self 46 | end 47 | else 48 | yield self 49 | end 50 | rescue SQLite3::BusyException => e 51 | DBlog::sto("SQLite3::BusyException tran = #{tran.to_s} #{ecount}") 52 | begin 53 | @db.rollback() if roll == true 54 | rescue 55 | DBlog::sto("rollback fail #{$!}") 56 | end 57 | if ecount > 59 58 | Commlib::errPrint( "SQLite3::BusyException exit", $!, e ) 59 | return 60 | else 61 | #Commlib::errPrint( "SQLite3::BusyException retry", $!, e ) 62 | ecount += 1 63 | sleep( 1 ) 64 | DBlog::sto("retry") 65 | retry 66 | end 67 | rescue => e 68 | Commlib::errPrint( "SQLite3::another error", $!, e ) 69 | begin 70 | @db.rollback() if roll == true 71 | rescue 72 | DBlog::sto("rollback fail #{$!}") 73 | end 74 | return 75 | ensure 76 | close() 77 | end 78 | end 79 | 80 | def close 81 | if @db != nil 82 | @db.close() 83 | @db = nil 84 | end 85 | end 86 | 87 | def execute( *args ) 88 | @db.execute( *args ) 89 | end 90 | 91 | #def transaction( mode = :immediate ) # :deferred or :immediate 92 | # @db.transaction( mode ) do 93 | # yield 94 | # end 95 | #end 96 | 97 | def prepare( str ) 98 | @db.prepare( str ) 99 | end 100 | 101 | # 102 | # データがあるか 103 | # 104 | def getDB( fname ) 105 | sql = "select * from data where fname = ?;" 106 | @db.execute( sql, fname ) do |row| 107 | return row 108 | end 109 | return nil 110 | end 111 | 112 | 113 | def getDBbyNmaeLike( *names ) 114 | q = "" 115 | names.each do |name| 116 | q += "%#{name}%" 117 | end 118 | sql = "select DISTINCT * from data where fname like '#{q}' order by fname;" 119 | return @db.execute( sql ) 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /src/timer_main.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'pp' 4 | require 'optparse' 5 | 6 | base = File.dirname( $0 ) 7 | [ ".", "..","src", base ].each do |dir| 8 | if test( ?f, dir + "/require.rb") 9 | $: << dir 10 | $baseDir = dir 11 | end 12 | end 13 | RewriteConst = true 14 | require 'require.rb' 15 | 16 | $debug = Debug 17 | OptionParser.new do |opt| 18 | opt.on('--debug') {|v| $debug = true } 19 | opt.parse!(ARGV) 20 | end 21 | 22 | reopenSTD( StdoutT, StderrT ) 23 | 24 | # 25 | # 終了処理 26 | # 27 | def endParoc() 28 | DBlog::puts( "endParoc() #{$httpd_pid}" ) 29 | if $rec_pid != nil 30 | $rec_pid.each_pair do |pid, v | 31 | if pid != nil 32 | begin 33 | Process.kill(:HUP, pid ); 34 | Process.waitpid( pid, Process::WNOHANG ) 35 | rescue Errno::ECHILD, Errno::ESRCH 36 | end 37 | DBlog::puts( "timer:endParoc() kill #{pid}" ) 38 | end 39 | end 40 | end 41 | sleep(1) 42 | exit 43 | end 44 | 45 | $rec_pid = {} # 子プロセス の pid 46 | $mutex = Mutex.new 47 | $tunerArray = TunerArray.new 48 | 49 | # 50 | # :CHLD のハンドラ 51 | # 52 | def childWait() 53 | #DBlog::sto("childWait()") 54 | Thread.new do 55 | sleep(10) # 時間差をつける 56 | $rec_pid.keys.each do |k| 57 | if $rec_pid[k] == true 58 | begin 59 | if Process.waitpid( k, Process::WNOHANG ) != nil 60 | DBlog::sto("timer:childWait() pid=#{k} Terminated") # 成仏 61 | $rec_pid.delete(k) 62 | end 63 | rescue Errno::ECHILD, Errno::ESRCH 64 | $rec_pid.delete(k) 65 | end 66 | end 67 | end 68 | end 69 | end 70 | 71 | # 72 | # メモリの使用量調査 73 | # 74 | def memSpace( db = nil ) 75 | require 'objspace' 76 | mem = ObjectSpace.memsize_of_all * 0.001 * 0.001 77 | rss = `ps -o rss= -p #{Process.pid}`.to_i * 0.001 78 | gcc = GC.count 79 | tmp = sprintf("memsize_of_all=%.2f MB ; RSS=%.2f MB ; GCC=%d",mem,rss, gcc) 80 | DBlog::debug(db, tmp ) 81 | end 82 | 83 | 84 | setTrap() 85 | 86 | File.open( TimerPidFile, "w") do |fp| 87 | fp.puts( Process.pid ) 88 | end 89 | 90 | DBlog::info(nil,"timer_main start #{Commlib::getVer()}") 91 | EpgLock::unlock() 92 | 93 | # 94 | # tool check 95 | # 96 | tools = [ Recpt1_cmd, Epgdump ] 97 | tools.each do |tool| 98 | unless test( ?x, tool ) 99 | foundF = false 100 | ENV["PATH"].split(/:/).each do |p| 101 | if test( ?x, "#{p}/#{tool}" ) 102 | foundF = true 103 | break 104 | end 105 | end 106 | if foundF == false 107 | DBlog::error( nil, "Error: #{tool} not found" ) 108 | end 109 | end 110 | end 111 | 112 | # 113 | # チューナーデバイスの読み書き権限のチェック 114 | # 115 | devList = ( DeviceList_GR + DeviceList_BSCS + DeviceList_GBC ) 116 | DeviceChk.new.devRWchk( devList ) 117 | 118 | # 119 | # メモリ使用量のモニタ 120 | # 121 | if $debug == true and Debug_mem == true 122 | Thread.new do 123 | while true 124 | memSpace( nil ) 125 | sleep( 600 ) 126 | end 127 | end 128 | end 129 | 130 | tm = Timer.new 131 | tm.start() 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/views/rsv_list.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 予約一覧 6 | # 7 | 8 | 9 | class ReservationList 10 | 11 | def initialize( ) 12 | @autoRsv = {} # 自動予約のIDリスト 13 | end 14 | 15 | def getData() 16 | reserve = DBreserve.new() 17 | programs = DBprograms.new 18 | filter = DBfilter.new 19 | 20 | now = Time.now.to_i 21 | DBaccess.new().open do |db| 22 | if (row = filter.select( db, type: FilConst::AutoRsv )) != nil 23 | row.each do |r| 24 | @autoRsv[ r[:id].to_i ] = true 25 | end 26 | end 27 | r = reserve.selectSP( db, tend: now) 28 | return r 29 | end 30 | nil 31 | end 32 | 33 | def printTD( str, clas: nil, id: nil, rid: nil, style: nil ) 34 | attr = "" 35 | attr += %Q{class="#{clas.join(" ")}" } if clas != nil 36 | attr += %Q{id="#{id}" } if id != nil 37 | attr += %Q{rid="#{rid}" } if rid != nil 38 | attr += %Q{style="#{style}" } if style != nil 39 | %Q{ #{str} } 40 | end 41 | 42 | def printTR( data, id: nil, clas: nil ) 43 | attr = [] 44 | attr << %Q(class="#{clas.join(" ")}") if clas != nil 45 | attr << %Q(id="#{id}") if id != nil 46 | 47 | a = [ %Q{ } ] 48 | a += data 49 | a << %Q{ } 50 | a.join("\n") 51 | end 52 | 53 | # 54 | # データの表示 55 | # 56 | def printTable() 57 | r = [] 58 | 59 | data = getData() 60 | if data != nil 61 | count = 1 62 | clas = %w( nowrap ) #item 63 | data.each do |t| 64 | time = Commlib::stet_to_s( t[:start], t[:end] ) 65 | if t[:type] == 0 66 | type = "手動" 67 | else 68 | if @autoRsv[ t[:keyid] ] == true 69 | type = %Q( 自動 ) 70 | else 71 | type = "自動(除)" 72 | end 73 | end 74 | 75 | bg = nil 76 | id = nil 77 | cate = t[:category] 78 | bg = %Q(color#{cate}) 79 | clasS = %w( nowrap ) 80 | 81 | ( stat, clasS, bg, recf ) = Commlib::statAna( t, clasS, bg ) 82 | 83 | title = %Q( #{t[:title]} ) 84 | st = ( Time.at(t[:start]) - 3600 ).strftime("%Y-%m-%d/%H") 85 | day = %Q( #{time[0]} ) 86 | time2 = %Q( #{time[1]} #{time[2]} ) 87 | name2 = %Q( #{t[:name]} ) 88 | td = [] 89 | td << printTD( count, clas: clas ) 90 | td << printTD( name2,clas: clas ) 91 | td << printTD( day,clas: clas ) 92 | td << printTD( time2,clas: clas ) 93 | td << printTD( stat,clas: clasS, id: id ) 94 | td << printTD( type,clas: clas ) 95 | td << printTD( title,clas: clas ) 96 | r << printTR( td, clas: [ bg ] ) 97 | count += 1 98 | end 99 | end 100 | r.join("\n") 101 | end 102 | end 103 | 104 | if File.basename($0) == "reservationList.rb" 105 | 106 | 107 | case ARGV[0] 108 | when "1" 109 | puts( pt.printTable( )) 110 | end 111 | 112 | end 113 | 114 | -------------------------------------------------------------------------------- /src/views/monitor.slim: -------------------------------------------------------------------------------- 1 | / 2 | / モニター 3 | / 4 | 5 | - require_relative 'monitor.rb' 6 | - pt = Monitor.new() 7 | - pt.createData() 8 | 9 | css: 10 | td,th { 11 | padding: 5px ; 12 | } 13 | label { 14 | margin: 10px ; 15 | color: black; 16 | font-size: 1em; 17 | } 18 | .inline-block { 19 | display: inline-block; /* インラインブロック要素にする */ 20 | padding: 0px; 21 | margin: 0px 2px 0px 2px; 22 | } 23 | .pagination { 24 | margin-left: 3em; 25 | } 26 | #live { 27 | margin-left: 1em; 28 | } 29 | #band { 30 | margin-top: 1em; 31 | } 32 | 33 | script src="https://cdn.jsdelivr.net/hls.js/latest/hls.min.js" 34 | 35 | div 36 | video#live width="#{MonitorWidth}" crossOrigin="anonymous" autoplay="autoplay" controls="controls" 37 | 38 | div.row 39 | div#band 40 | ul.tabs 41 | - pt.band.each do |band| 42 | li.tab.col.s2 43 | a href="##{band}" 44 | | #{pt.bandname[band]} 45 | 46 | div 47 | - pt.band.each do |band| 48 | div.col.s8 id = band 49 | == pt.data[band] 50 | 51 | 52 | 53 | div id="sample-dialog1" title="wait" style="display:none;" 54 | p 準備中。 しばらくお待ち下さい。 55 | 56 | 57 | 58 | javascript: 59 | 60 | 61 | $(".hls").click(function(event){ 62 | event.preventDefault(); 63 | //$("#sample-dialog1").show(); 64 | $("#sample-dialog1").dialog({ 65 | modal: true, 66 | maxWidth: 1200, 67 | width: 600, 68 | buttons: { //ボタン 69 | "閉じる": function() { 70 | $(this).dialog("close"); 71 | } 72 | } 73 | }); 74 | 75 | url = $(this).attr('href'); 76 | $.ajax({ 77 | url: url, 78 | type:"GET", 79 | async: true, 80 | timeout: 30000 81 | }) 82 | .done( function(results ){ 83 | //console.log("URL : " + url); 84 | //console.log("results : " + results); 85 | setTimeout(function(){ 86 | location.href='/monitor'; 87 | },3000); 88 | }).fail(function (jqXHR, textStatus, errorThrown) { 89 | // 通信失敗時の処理 90 | console.log("ajax通信に失敗しました"); 91 | console.log("jqXHR : " + jqXHR.status); // HTTPステータスが取得 92 | console.log("textStatus : " + textStatus); // タイムアウト、パースエラー 93 | console.log("errorThrown: " + errorThrown.message); // 例外情報 94 | console.log("URL : " + url); 95 | //alert("起動失敗"); 96 | $("#sample-dialog1").html("

起動に失敗しました。"); 97 | }) 98 | return false; 99 | }); 100 | 101 | 102 | 103 | if (Hls.isSupported()) { 104 | var video = document.getElementById('live'); 105 | var hls = new Hls(); 106 | hls.loadSource('/stream/playlist.m3u8'); 107 | hls.attachMedia(live); 108 | hls.on(Hls.Events.MANIFEST_PARSED, function () { 109 | video.play(); 110 | }); 111 | } 112 | function jumptolatest() { 113 | document.getElementById("live").currentTime = 99999999; 114 | } 115 | setTimeout("jumptolatest()", 2000); 116 | 117 | $(document).ready(function(){ 118 | $('.tabs').tabs(); 119 | }); 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/model/EpgNearCh.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # 4 | # 直近(10分)の予約がある物理チャンネルを返す。 5 | # 前回の履歴から重複はしないように 6 | # 7 | 8 | class EpgNearCh 9 | 10 | @@updT = { } # chid 毎の EPG更新時間(短縮) 11 | 12 | def initialize( t = 600 ) 13 | @sepTime = t # 連続とみなす予約間隔 14 | end 15 | 16 | # 17 | # 直近に録画番組がある局のEPG 取得のために、更新時間を細工 18 | # @sepTime は予約間の隙間の許容値 19 | # 20 | def check( ) 21 | 22 | #DBlog::stoD("EpgNearCh::check()") 23 | st = et = nil 24 | reserve = DBreserve.new 25 | phchid = DBphchid.new 26 | channel = DBchannel.new 27 | keyval = DBkeyval.new 28 | nearTime = Time.now.to_i + 60 * 10 # 直近とは 10分以内 29 | now = Time.now.to_i 30 | list = [] 31 | res = [] 32 | 33 | DBaccess.new().open( tran: true ) do |db| 34 | 35 | row = reserve.selectSP( db, stat: RsvConst::Normal, order: "order by start" ) 36 | if row.size == 0 or ( row.first[:start] > nearTime ) 37 | return [] 38 | end 39 | 40 | # 直近の連続した予約を抽出 41 | row.each do |r| 42 | if st == nil 43 | st = r[:start] 44 | et = r[:end] 45 | list << r 46 | else 47 | if r[:start].between?( st, et + @sepTime ) 48 | et = et > r[:end] ? et : r[:end] 49 | list << r 50 | else 51 | break 52 | end 53 | end 54 | end 55 | 56 | updtime = {} # chid 毎の EPG更新時間(正規) 57 | row = phchid.select( db ) 58 | row.each do |tmp| 59 | chid = tmp[:chid] 60 | if updtime[chid] == nil or updtime[chid] < tmp[:updatetime] 61 | updtime[chid] = tmp[:updatetime] 62 | end 63 | end 64 | 65 | chid2phch = channel.makeChid2Phch(db) 66 | 67 | list.each do |r| 68 | chid = r[:chid] 69 | phch = chid2phch[ chid ] 70 | flag = false 71 | if updtime[chid] < ( now - 10 * 60 ) 72 | if @@updT[chid] == nil or @@updT[chid] < ( now - 10 * 60 ) 73 | flag = true 74 | end 75 | end 76 | # if $debug == true 77 | # sa1 = r[:start] - now 78 | # sa2 = now - updtime[chid] 79 | # sa3 = now - ( @@updT[chid] == nil ? now : @@updT[chid] ) 80 | # mark = flag == true ? "+" : " " 81 | # tmp = sprintf("%s %6s 録 %5d 秒前 EPG1 %5d 秒前 EPG2 %5d 秒前 %s", 82 | # mark, phch, sa1,sa2,sa3, r[:title]) 83 | # DBlog::stoD(tmp) 84 | # end 85 | if flag == true 86 | @@updT[chid] = now 87 | res << phch 88 | end 89 | end 90 | res.uniq! 91 | 92 | if res.size > 0 93 | DBlog::debug( db,"EpgNearCh #{res.join(" ")}" ) 94 | end 95 | end 96 | return res 97 | end 98 | 99 | 100 | def ppp() 101 | pp @@updT 102 | end 103 | 104 | end 105 | 106 | 107 | if File.basename($0) == "EpgNearCh.rb" 108 | base = File.dirname( $0 ) 109 | [ ".", "..","src", base ].each do |dir| 110 | if test( ?f, dir + "/require.rb") 111 | $: << dir 112 | $baseDir = dir 113 | end 114 | end 115 | require 'require.rb' 116 | 117 | $debug = true 118 | ge = EpgNearCh.new 119 | pp ge.check() 120 | ge.ppp() 121 | 122 | exit 123 | 124 | 125 | end 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/lib/deviceChk.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'yaml' 4 | require 'lib/YamlWrap.rb' 5 | 6 | # 7 | # デバイスのチェック 8 | # 9 | class DeviceChk 10 | 11 | attr_reader :data 12 | 13 | def initialize( ) 14 | end 15 | 16 | def run( ) 17 | @data = DeviceChkData.new 18 | yaml = YAML.dump(@data) 19 | 20 | File.open( DeviceChkFN, "w") do |fp| 21 | fp.puts( yaml ) 22 | end 23 | 24 | return @data 25 | end 26 | 27 | def load( ) 28 | @data = nil 29 | if test( ?f, DeviceChkFN ) 30 | File.open( DeviceChkFN, "r") do |fp| 31 | @data = YamlWrap.load_file(fp) 32 | end 33 | else 34 | run() 35 | end 36 | return @data 37 | end 38 | 39 | # 40 | # パーミッションのチェック 41 | # 42 | def devRWchk( list ) 43 | list.sort.each do |devfn| 44 | if devfn =~ /frontend0/ 45 | base = File.dirname( devfn ) 46 | chkRW( File.join( base,"demux0" )) 47 | chkRW( File.join( base,"dvr0" ) ) 48 | chkRW( devfn ) 49 | else 50 | chkRW( devfn ) 51 | end 52 | end 53 | end 54 | 55 | def chkRW( fn ) 56 | if FileTest.chardev?( fn ) or FileTest.blockdev?( fn ) 57 | if FileTest.writable?( fn ) == true and FileTest.readable?( fn ) == true 58 | # DBlog::sto("device R/W check OK #{fn}") 59 | else 60 | DBlog::error(nil, "Error: device R/W check NG #{fn}") 61 | end 62 | else 63 | DBlog::error(nil, "Error: device file not found #{fn}") 64 | end 65 | end 66 | 67 | 68 | end 69 | 70 | 71 | class DeviceChkData 72 | 73 | attr_reader :listGR, :listBC, :listGBC, :total 74 | 75 | GR = "地デジ" 76 | BC = "BS/CS" 77 | GBC = "GR/BS/CS" 78 | 79 | def makeDevName( hina, nums, type ) 80 | nums.each do |n| 81 | devfn = "/dev/" + sprintf( hina, n ) 82 | if FileTest.chardev?( devfn ) or FileTest.blockdev?(devfn) 83 | DBlog::sto("device found #{devfn} #{type}") 84 | case type 85 | when GR then @listGR << devfn 86 | when BC then @listBC << devfn 87 | when GBC then @listGBC << devfn 88 | end 89 | else 90 | break 91 | end 92 | end 93 | end 94 | 95 | def initialize( ) 96 | 97 | @listGR = [] # 地デジ 98 | @listBC = [] # BS/CS 99 | @listGBC = [] # GR/BS/CS 100 | 101 | numT = [2, 3, 6, 7, 10, 11, 14, 15 ] # 地デジ 102 | numS = [0, 1, 4, 5, 8, 9, 12, 13, ] # BS/CS 103 | num3 = Array.new(16){|n| n } # 3波 104 | 105 | numT2 = [1, 3, 5, 7, ] # dvb ドライバー 地デジ 106 | numS2 = [0, 2, 4, 6, ] # dvb ドライバー BS/CS 107 | 108 | makeDevName( "px4video%d",numS, BC ) # PX-Q3U4/Q3PE4/Q3PE5 109 | makeDevName( "pt1video%d",numS, BC ) # PT1/PT2 110 | makeDevName( "pt3video%d",numS, BC ) # PT3 111 | 112 | makeDevName( "px4video%d",numT, GR ) # PX-Q3U4/Q3PE4/Q3PE5 113 | makeDevName( "pt1video%d",numT, GR ) # PT1/PT2 114 | makeDevName( "pt3video%d",numT, GR ) # PT3 115 | 116 | makeDevName( "pxmlt5video%d", num3, GBC) # PX-MLT5PE 117 | makeDevName( "pxmlt8video%d", num3, GBC) # PX-MLT8PE 118 | makeDevName( "isdb2056video%d",num3, GBC) # DTV02-1T1S-U 119 | makeDevName( "isdb6014video%d",num3, GBC) # DTV02A-4TS-P 120 | 121 | makeDevName( "dvb/adapter%d/frontend0", numT2, GR ) # dvb 122 | makeDevName( "dvb/adapter%d/frontend0", numS2, BC ) # dvb 123 | 124 | @total = @listGR.size + @listBC.size + @listGBC.size 125 | 126 | end 127 | 128 | end 129 | 130 | if File.basename($0) == "deviceChk.rb" 131 | DeviceChk.new 132 | end 133 | 134 | 135 | -------------------------------------------------------------------------------- /src/views/style.sass: -------------------------------------------------------------------------------- 1 | 2 | body 3 | padding: 3px 4 | background: #d3d3d3 5 | img 6 | margin: 10px 7 | border: 5px 8 | solid: #000000 9 | 10 | .fss 11 | font-size: 90% 12 | padding: 5px 13 | 14 | .nowrap 15 | white-space: nowrap 16 | 17 | a 18 | color: navy 19 | 20 | tr.color0, div.color0 21 | border: solid 1px #999 22 | background-color: #cfe5fc !important 23 | 24 | tr.color1, div.color1 25 | border: solid 1px #999 26 | background-color: #d1d4e9 !important 27 | 28 | tr.color2, div.color2 29 | border: solid 1px #999 30 | background-color: #ffdde2 !important 31 | 32 | tr.color3, div.color3 33 | border: solid 1px #999 34 | background-color: #f1cfef !important 35 | 36 | tr.color4, div.color4 37 | border: solid 1px #999 38 | background-color: #bbdefb !important 39 | 40 | tr.color5, div.color5 41 | border: solid 1px #999 42 | background-color: #b2dfdb !important 43 | 44 | tr.color6, div.color6 45 | border: solid 1px #999 46 | background-color: #c8e6c9 !important 47 | 48 | tr.color7, div.color7 49 | border: solid 1px #999 50 | background-color: #f0f4c3 !important 51 | 52 | tr.color8,div.color8 53 | border: solid 1px #999 54 | background-color: #ffe8c2 !important 55 | 56 | tr.color9, div.color9 57 | border: solid 1px #999 58 | background-color: #ffdccc !important 59 | 60 | tr.color10, div.color10 61 | border: solid 1px #999 62 | background-color: #e7dfd8 !important 63 | 64 | tr.color11, div.color11 65 | border: solid 1px #999 66 | background-color: #e2ffb9 !important 67 | 68 | tr.color12, div.color12 69 | border: solid 1px #999 70 | background-color: #aff0ce !important 71 | 72 | tr.color13, div.color13 73 | border: solid 1px #999 74 | background-color: #b7ffeb !important 75 | 76 | tr.color14, div.color14 77 | border: solid 1px #999 78 | background-color: #ffcac0 !important 79 | 80 | tr.color15, div.color15 81 | border: solid 1px #999 82 | background-color: #c2d1ff !important 83 | 84 | tr.color16, div.color16 85 | border: solid 1px #999 86 | background-color: #e6c8ff !important 87 | 88 | tr.color17, div.color17 89 | border: solid 1px #999 90 | background-color: #8c9eff !important 91 | 92 | tr.colorGray, div.colorGray 93 | border: solid 1px #999 94 | background-color: #aaaaaa !important 95 | 96 | td.alertR, div.alertR 97 | border: solid 3px red !important 98 | 99 | td.alertB, div.alertB 100 | border: solid 3px black !important 101 | 102 | td.alertBD, div.alertBD 103 | border: dashed 3px black !important 104 | 105 | td.alertGD, div.alertGD 106 | border: dashed 3px gray !important 107 | 108 | 109 | #in,#email_inline 110 | height: 2em 111 | 112 | #email_inline 113 | width: 50% 114 | margin-left: 1em 115 | 116 | #title 117 | margin: 0.5em 0.5em 0.5em 0.5em 118 | font-size: 1.5em 119 | 120 | td.error 121 | color: crimson !important 122 | 123 | td.warn 124 | color: purple !important 125 | 126 | td.atte 127 | color: indigo !important 128 | 129 | /* 上に戻るボタン 130 | .pagetop 131 | display: none 132 | position: fixed 133 | bottom: 30px 134 | right: 15px 135 | a 136 | display: block 137 | background-color: #ccc 138 | text-align: center 139 | color: #222 140 | font-size: 24px 141 | text-decoration: none 142 | padding: 5px 10px 143 | filter: alpha(opacity = 50) 144 | -moz-opacity: 0.5 145 | opacity: 0.5 146 | &:hover 147 | display: block 148 | background-color: #b2d1fb 149 | text-align: center 150 | color: #fff 151 | font-size: 24px 152 | text-decoration: none 153 | padding: 5px 10px 154 | filter: alpha(opacity = 50) 155 | -moz-opacity: 0.5 156 | opacity: 0.5 157 | 158 | -------------------------------------------------------------------------------- /src/TV/misc.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # 4 | # raspirecTV.rb 関連 5 | # 6 | 7 | # 8 | # チャンネル情報 9 | # 10 | class ChList 11 | 12 | def initialize() 13 | @phch = {} 14 | @chList = {} 15 | channel = DBchannel.new 16 | DBaccess.new().open do |db| 17 | row = channel.select( db, order: "order by band_sort,svid" ) 18 | row.each do |r| 19 | next if r[:updatetime] == -1 20 | next if r[:skip] == 1 21 | chid = r[:chid ] 22 | tmp = Commlib::makePhCh( r ) 23 | band = r[:band] 24 | @phch[ chid ] = ChInfo.new(phch: tmp, svid: r[:svid], band: band, chname: r[:name] ) 25 | @chList[ band ] ||= {} 26 | @chList[ band ][ r[:name]] = chid 27 | end 28 | end 29 | end 30 | 31 | def getChList() 32 | return @chList 33 | end 34 | 35 | def getPhCh( chid ) 36 | return @phch[ chid ] 37 | end 38 | end 39 | 40 | # 41 | # 番組情報の取得 42 | # 43 | class Prog 44 | 45 | attr_reader :data 46 | def getData( chid ) 47 | if @updTime == nil or @updTime < Time.now.to_i 48 | getInfo() 49 | end 50 | return @data[ chid ] 51 | end 52 | 53 | def getDetail( chid ) 54 | tmp = @data[ chid ] 55 | if tmp != nil 56 | ret = "\n" + tmp.prog_name + "\n\n" 57 | ret += Commlib::stet_to_s( tmp.start, tmp.endt ).join(" ") + "\n\n" 58 | if tmp.prog_detail != nil 59 | ret += tmp.prog_detail + "\n" 60 | end 61 | if tmp.prog_extdetail != nil and tmp.prog_extdetail.strip != "" 62 | if ( data = YamlWrap.load( tmp.prog_extdetail)).size > 0 63 | ret += "\n----- 詳細情報 ------\n" 64 | data.each do |tmp| 65 | title = tmp[ "item_description" ] 66 | item = tmp[ "item" ] 67 | ret += "<<< #{title} >>>\n" 68 | ret += item + "\n\n" 69 | end 70 | end 71 | end 72 | else 73 | ret = "" 74 | end 75 | 76 | return ret 77 | end 78 | 79 | def getInfo( ) 80 | @data = {} 81 | @updTime = nil 82 | programs = DBprograms.new 83 | DBaccess.new().open do |db| 84 | now = Time.now.to_i 85 | row = programs.selectSP( db, tstart: now, tend: now ) 86 | row.each do |r1| 87 | chid = r1[:chid] 88 | @data[chid] = ChInfo.new( prog_name: r1[:title], 89 | prog_detail: r1[:detail], 90 | prog_extdetail: r1[:extdetail], 91 | start: r1[:start], 92 | endt: r1[:end], 93 | chname: r1[:name] 94 | ) 95 | if @updTime == nil or @updTime > r1[:end] 96 | @updTime = r1[:end] 97 | #pp Time.at( @updTime ).to_s + " " + r1[:title] 98 | end 99 | end 100 | end 101 | end 102 | end 103 | 104 | class ChInfo 105 | attr_accessor :prog_name, :prog_detail, :prog_extdetail, :phch, :svid, :band 106 | attr_accessor :start, :endt, :chname 107 | 108 | def initialize( prog_name: nil, 109 | prog_detail: nil, 110 | prog_extdetail: nil, 111 | start: nil, 112 | endt: nil, 113 | phch: nil, 114 | svid: nil, 115 | band: nil, 116 | chname: nil 117 | ) 118 | @prog_name = prog_name 119 | @prog_detail = prog_detail 120 | @prog_extdetail = prog_extdetail 121 | @phch = phch 122 | @svid = svid 123 | @band = band 124 | @start = start 125 | @endt = endt 126 | @chname = chname 127 | 128 | end 129 | end 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /src/model/PacketChk.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # パケットチェック 6 | # 7 | 8 | class PacketChk < FileCopy # FileCopy を流用 9 | 10 | 11 | def initialize( ) 12 | @reserve = DBreserve.new 13 | end 14 | 15 | def start( time_limit ) 16 | lock() { start2( time_limit ) } 17 | end 18 | 19 | def start2( time_limit ) 20 | 21 | list = nil 22 | DBaccess.new().open( tran: true ) do |db| 23 | list = @reserve.getPacketChk( db ) 24 | DBkeyval.new.upsert( db, StatConst::KeyName, StatConst::PacketChk ) 25 | end 26 | DBlog::sto( "Packet Check 開始 #{list.size}") if list.size > 0 27 | 28 | abNormal = [] 29 | list.each do |l| 30 | 31 | if $recCount > 0 # 録画中は実行しない 32 | DBlog::sto("rec now -> Packet Check 停止" ) 33 | break 34 | end 35 | next if l[:fname] == nil 36 | 37 | path = TSDir + "/" 38 | if l[:subdir] != nil and l[:subdir] != "" 39 | subdir2 = Commlib::normStr( l[:subdir] ) 40 | path += subdir2.sub(/^\//,'').sub(/\/$/,'').strip + "/" 41 | end 42 | path += l[:fname] 43 | #DBlog::sto( path ); 44 | 45 | if test( ?f , path ) 46 | size = File.size( path ) 47 | t = (Time.now + (size / (PacketChk_rate * 2 ** 20))).to_i 48 | if t > time_limit 49 | DBlog::stoD( sprintf("time limit %s > %s", t, time_limit )) 50 | next 51 | end 52 | 53 | ( drer, pcr, speed ) = run( path ) 54 | pcr = pcr == "OK" ? 0 : 1 if pcr != 0 55 | 56 | if speed > 0 57 | DBaccess.new().open( tran: true ) do |db| 58 | tmp = sprintf("PacketChk 終了: %s (%.1f Mbyte/sec)", l[:fname], speed ) 59 | DBlog::info(db,tmp) 60 | val = @reserve.makeDropNum( drer, pcr, 0 ) 61 | @reserve.updateStat( db, l[:id], dropNum: val ) 62 | end 63 | else 64 | tmp = sprintf("PacketChk 失敗: %s", l[:fname] ) 65 | DBlog::warn(nil,tmp) 66 | abNormal << [ l[:id], l[:fname] ] 67 | end 68 | else 69 | tmp = sprintf("PacketChk 失敗(not found): %s", l[:fname] ) 70 | DBlog::sto( tmp ) 71 | abNormal << [ l[:id], l[:fname] ] 72 | end 73 | end 74 | 75 | DBaccess.new().open( tran: true ) do |db| 76 | if abNormal.size > 0 77 | abNormal.each do |tmp| 78 | ( id, fname ) = tmp 79 | val = @reserve.makeDropNum( 0, 0, 1 ) 80 | @reserve.updateStat( db, id, dropNum: val ) 81 | end 82 | end 83 | DBkeyval.new.upsert( db, StatConst::KeyName, StatConst::None ) 84 | end 85 | 86 | 87 | end 88 | 89 | # 90 | # tspacketchk の実行 91 | # 92 | def run( path ) 93 | 94 | args = [ PacketChk_cmd ] 95 | args += PacketChk_opt.split().push( path ) 96 | drer = pcr = speed = 0 97 | 98 | logfname = path + ".chk" 99 | File.open(logfname, File::RDWR|File::CREAT, 0644) do |fl| 100 | if fl.flock(File::LOCK_EX|File::LOCK_NB) == false 101 | DBlog::debug( nil,"chk log locked(#{path})") 102 | else 103 | IO.popen( args, "r") do |io| 104 | io.each_line do |line| 105 | if line =~ /drop\+error\s+=\s+(\d+)/ 106 | drer = $1.to_i 107 | elsif line =~ /Check Time.*?\(([\d\.]+) Mbyte\/sec/ 108 | speed = $1.to_f 109 | elsif line =~ /PCR Wrap-around check\s+=\s+(OK|NG)/i 110 | pcr = $1 111 | end 112 | fl.puts( line ) 113 | end 114 | end 115 | end 116 | end 117 | size = File.size( logfname ) 118 | DBlog::stoD( "PacketCheck end #{drer},#{pcr},#{speed},#{size}") 119 | return [ drer,pcr,speed] 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /src/model/Const.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 定数定義 6 | # 7 | 8 | module Const 9 | 10 | ProgName = "raspirec" 11 | ProgVer = "Ver 1.5.3" 12 | GitTag = "Ver1.5.3" 13 | 14 | GR = "GR" 15 | GRJ = "地デジ" 16 | BS = "BS" 17 | CS = "CS" 18 | BSCS = "BSCS" 19 | GBC = "GRBSCS" 20 | Wday = { 0 => "(日)", 21 | 1 => "(月)", 22 | 2 => "(火)", 23 | 3 => "(水)", 24 | 4 => "(木)", 25 | 5 => "(金)", 26 | 6 => "(土)", } 27 | 28 | FAtype = "fa_type" 29 | LastEpgTime = "lastepgtime" 30 | GB = 1024 * 1024 * 1024 31 | MB = 1024 * 1024 32 | PlayListFname = "playlist.m3u8" # playList ファイル名 33 | 34 | end 35 | 36 | module StrConst 37 | Jitan = "チューナーが競合した場合に録画時間の短縮を許可する。" 38 | NotUse = "この予約を無効とする。" 39 | Dedupe = "過去に録画したタイトルと一致した場合は無効にする。" 40 | FreeOnly = "無料放送のみ" 41 | end 42 | 43 | 44 | # 45 | # 予約関係 46 | # 47 | module RsvConst 48 | 49 | Normal = 0 # 予約中 正常 50 | Conflict = 1 # 予約中 競合 51 | NormalEnd = 2 # 正常終了 52 | AbNormalEnd= 3 # 異常終了 53 | RecNow = 4 # 録画中 54 | RecStop = 5 # 番組消失 55 | RecStop2 = 6 # 手動操作による録画中止 56 | NotUseA = 7 # 予約無効(自動予約) 57 | NotUse = 9 # 予約無効(手動) 58 | WaitStat = [0,1,4,7,9] # 予約中、録画中の status 59 | EndStat = [2,3,5] # 終了した status 60 | RecStat = [4] # 録画中の status 61 | ActStat = [0,1,4] # 有効な status 62 | AllStat = [0,1,2,3,4,5,6,7,9] # All status 63 | 64 | Manual = 0 # 手動予約 65 | Auto = 1 # 自動予約 66 | #NotUse = 1 # 不使用 67 | #Use = 0 # 使用 68 | JitanOn = 0 # 時短許可 69 | JitanOff = 1 # 時短不許可 70 | JitanEOn = 0 # 録画時時短を実行する。 71 | JitanEOff = 1 # 〃 しない 72 | FO = 1 # 無料放送のみ制限する 73 | Off = 0 # off 74 | Dedupe = 1 # 重複予約を無効化 75 | Ftp_Complete = 1 # 転送完了 76 | Ftp_AbNormal = 2 # 異常終了 77 | HashSetFlag = 999 # title の hash を設定したかのフラグ 78 | end 79 | 80 | # 81 | # フィルター関係 82 | # 83 | module FilConst 84 | 85 | Filter = 0 # フィルター 86 | AutoRsv = 1 # 自動予約 87 | RegexOn = 1 # 正規表現 88 | RegexOff = 0 # 単純文字列検索 89 | TypeMan = 0 # 手動予約 90 | TypeAuto = 1 # 自動予約 91 | JitanOn = 0 # 時短許可 92 | JitanOff = 1 # 時短不許可 93 | BandGR = 1 # GR 94 | BandBS = 2 # BS 95 | BandCS = 4 # CS 96 | BandALL = 7 # GR + BS + CS 97 | TargetT = 0 # 検索対象 = タイトル 98 | TargetTS = 1 # 検索対象 = タイトル+概要 99 | SeachMax = 512 # 検索最大値 100 | 101 | end 102 | 103 | 104 | # 105 | # ステータス関係 106 | # 107 | module StatConst 108 | KeyName = "Status" 109 | None = 0 110 | FileCopy = 1 111 | EPGget = 2 112 | RecNow = 4 113 | PacketChk = 5 114 | end 115 | 116 | # 117 | # 自動予約一覧の sort type 118 | # 119 | module ARSort 120 | Title = 0 121 | Reg = 1 122 | Cate = 2 123 | Num = 3 124 | Reverse= 4 125 | Off = 0 # off 126 | On = 1 # on 127 | KeyNameT = "ARSortType" 128 | KeyNameR = "ARSortReverse" 129 | end 130 | 131 | # 132 | # フィルター一覧の sort type 133 | # 134 | module FilSort 135 | KeyNameT = "FilSortType" 136 | KeyNameR = "FilSortReverse" 137 | end 138 | -------------------------------------------------------------------------------- /src/views/monitor.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # HLS モニター 6 | # 7 | 8 | 9 | class Monitor 10 | 11 | 12 | attr_reader :band, :list, :data, :bandname 13 | 14 | def initialize( ) 15 | 16 | @base_url = "/monitor" 17 | 18 | end 19 | 20 | # 21 | # 表示データ生成 22 | # 23 | def createData(recFlag = true) 24 | @list = getChData( ) 25 | @band = @list.keys 26 | @bandname = { "rec" => "録画中", 27 | "file" => "録画済み", 28 | } 29 | @band.each do |band| 30 | band2 = band == "GR" ? "地デジ" : band 31 | @bandname[ band ] = band2 32 | end 33 | @data = make_table( @list ) 34 | 35 | if recFlag == true 36 | if ( rec = getRecData()) != nil 37 | title = "rec" 38 | @band.unshift( title ) 39 | @data[ title ] = rec 40 | end 41 | end 42 | end 43 | 44 | # 45 | # 録画済みTSファイル一覧(不使用) 46 | # 47 | def getFileData() 48 | paths = {} 49 | Find.find( TSDir ) do |path| 50 | if test(?f, path ) 51 | if path =~ /\.ts$/ 52 | if File.size( path ) > 0 53 | fname = path.sub( TSDir + "/" , '' ) 54 | paths[ File.basename( fname ) ] = CGI.escape( fname ) 55 | end 56 | end 57 | end 58 | end 59 | if paths.size > 0 60 | ret = [] 61 | ret << %q(

    ) 62 | paths.keys.sort.each do |path| 63 | id = paths[ path ] 64 | ret << %Q(
  1. #{path} ) 65 | end 66 | ret << %q(
) 67 | return ret.join("\n") 68 | else 69 | return nil 70 | end 71 | end 72 | 73 | # 74 | # 録画中の番組を取得 75 | # 76 | def getRecData() 77 | paths = {} 78 | reserve = DBreserve.new 79 | DBaccess.new().open do |db| 80 | row = reserve.selectSP( db, stat: RsvConst::RecNow ) 81 | row.each do |l| 82 | path = TSDir + "/" 83 | if l[:subdir] != nil and l[:subdir] != "" 84 | subdir2 = Commlib::normStr( l[:subdir] ) 85 | path += subdir2.sub(/^\//,'').sub(/\/$/,'').strip + "/" 86 | end 87 | if l[:fname] != nil 88 | path += l[:fname] 89 | if test( ?f, path ) 90 | paths[ l[:fname] ] = l[:id] 91 | end 92 | end 93 | end 94 | end 95 | 96 | if paths.size > 0 97 | ret = [] 98 | ret << %q(
    ) 99 | paths.keys.each do |path| 100 | id = paths[ path ] 101 | ret << %Q(
  1. #{path} ) 102 | end 103 | ret << %q(
) 104 | return ret.join("\n") 105 | else 106 | return nil 107 | end 108 | end 109 | 110 | # 111 | # 放送局一覧 112 | # 113 | def make_table( list ) 114 | 115 | ret = {} 116 | list.keys.each do |band| 117 | tmp = [] 118 | tmp << %q() # class="striped" 119 | tmp << %q() 120 | 121 | n = 0 122 | list[ band ].each_pair do |k,v| 123 | tmp << %Q() 124 | n += 1 125 | if n > 4 126 | tmp << %q() 127 | tmp << %q() 128 | n = 0 129 | end 130 | end 131 | tmp << %q() 132 | tmp << %q(
#{k}
) 133 | 134 | ret[ band ] = tmp.join("\n") 135 | end 136 | return ret 137 | end 138 | 139 | 140 | # 141 | # channelデータの取得 142 | # 143 | def getChData( ) 144 | channel = DBchannel.new 145 | 146 | list = {} 147 | DBaccess.new().open do |db| 148 | row = channel.select( db, order: "order by band_sort,svid" ) 149 | row.each do |r| 150 | next if r[:updatetime] == -1 151 | next if r[:skip] == 1 152 | list[ r[:band] ] ||= {} 153 | list[ r[:band] ][ r[:name] ] = r[:chid ] 154 | end 155 | end 156 | list 157 | end 158 | 159 | 160 | end 161 | -------------------------------------------------------------------------------- /src/public/style.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | body { 3 | padding: 3px; 4 | background: #d3d3d3; } 5 | 6 | img { 7 | margin: 10px; 8 | border: 5px; 9 | solid: #000000; } 10 | 11 | .fss { 12 | font-size: 90%; 13 | padding: 5px; } 14 | 15 | .nowrap { 16 | white-space: nowrap; } 17 | 18 | a { 19 | color: navy; } 20 | 21 | tr.color0, div.color0 { 22 | border: solid 1px #999; 23 | background-color: #cfe5fc !important; } 24 | 25 | tr.color1, div.color1 { 26 | border: solid 1px #999; 27 | background-color: #d1d4e9 !important; } 28 | 29 | tr.color2, div.color2 { 30 | border: solid 1px #999; 31 | background-color: #ffdde2 !important; } 32 | 33 | tr.color3, div.color3 { 34 | border: solid 1px #999; 35 | background-color: #f1cfef !important; } 36 | 37 | tr.color4, div.color4 { 38 | border: solid 1px #999; 39 | background-color: #bbdefb !important; } 40 | 41 | tr.color5, div.color5 { 42 | border: solid 1px #999; 43 | background-color: #b2dfdb !important; } 44 | 45 | tr.color6, div.color6 { 46 | border: solid 1px #999; 47 | background-color: #c8e6c9 !important; } 48 | 49 | tr.color7, div.color7 { 50 | border: solid 1px #999; 51 | background-color: #f0f4c3 !important; } 52 | 53 | tr.color8, div.color8 { 54 | border: solid 1px #999; 55 | background-color: #ffe8c2 !important; } 56 | 57 | tr.color9, div.color9 { 58 | border: solid 1px #999; 59 | background-color: #ffdccc !important; } 60 | 61 | tr.color10, div.color10 { 62 | border: solid 1px #999; 63 | background-color: #e7dfd8 !important; } 64 | 65 | tr.color11, div.color11 { 66 | border: solid 1px #999; 67 | background-color: #e2ffb9 !important; } 68 | 69 | tr.color12, div.color12 { 70 | border: solid 1px #999; 71 | background-color: #aff0ce !important; } 72 | 73 | tr.color13, div.color13 { 74 | border: solid 1px #999; 75 | background-color: #b7ffeb !important; } 76 | 77 | tr.color14, div.color14 { 78 | border: solid 1px #999; 79 | background-color: #ffcac0 !important; } 80 | 81 | tr.color15, div.color15 { 82 | border: solid 1px #999; 83 | background-color: #c2d1ff !important; } 84 | 85 | tr.color16, div.color16 { 86 | border: solid 1px #999; 87 | background-color: #e6c8ff !important; } 88 | 89 | tr.color17, div.color17 { 90 | border: solid 1px #999; 91 | background-color: #8c9eff !important; } 92 | 93 | tr.colorGray, div.colorGray { 94 | border: solid 1px #999; 95 | background-color: #aaaaaa !important; } 96 | 97 | td.alertR, div.alertR { 98 | border: solid 3px red !important; } 99 | 100 | td.alertY, div.alertY { 101 | border: solid 3px yellow !important; } 102 | 103 | td.alertB, div.alertB { 104 | border: solid 3px black !important; } 105 | 106 | td.alertBD, div.alertBD { 107 | border: dashed 3px black !important; } 108 | 109 | td.alertGD, div.alertGD { 110 | border: dashed 3px gray !important; } 111 | 112 | #in, #email_inline { 113 | height: 2em; } 114 | 115 | #email_inline { 116 | width: 50%; 117 | margin-left: 1em; } 118 | 119 | #title { 120 | margin: 0.5em 0.5em 0.5em 0.5em; 121 | font-size: 1.5em; } 122 | 123 | td.error { 124 | color: crimson !important; } 125 | 126 | td.warn { 127 | color: purple !important; } 128 | 129 | td.atte { 130 | color: indigo !important; } 131 | 132 | /* 上に戻るボタン */ 133 | .pagetop { 134 | display: none; 135 | position: fixed; 136 | bottom: 30px; 137 | right: 15px; } 138 | .pagetop a { 139 | display: block; 140 | background-color: #ccc; 141 | text-align: center; 142 | color: #222; 143 | font-size: 24px; 144 | text-decoration: none; 145 | padding: 5px 10px; 146 | filter: alpha(opacity=50); 147 | -moz-opacity: 0.5; 148 | opacity: 0.5; } 149 | .pagetop a:hover { 150 | display: block; 151 | background-color: #b2d1fb; 152 | text-align: center; 153 | color: #fff; 154 | font-size: 24px; 155 | text-decoration: none; 156 | padding: 5px 10px; 157 | filter: alpha(opacity=50); 158 | -moz-opacity: 0.5; 159 | opacity: 0.5; } 160 | -------------------------------------------------------------------------------- /src/db/Log.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class DBlog 6 | 7 | Stdout = 0 # level 0 標準出力のみ、記録しない 8 | Debug = 1 # level 1 デバック 9 | Info = 2 # level 2 情報 10 | Attention = 3 # level 3 注意 11 | Warning = 4 # level 4 警告 12 | Error = 5 # level 5 エラー 13 | 14 | def initialize( ) 15 | @list = { 16 | id: "id", 17 | level: "level", 18 | time: "time", 19 | str: "str", 20 | } 21 | @para = [] 22 | @list.each_pair { |k,v| @para << v } 23 | @tbl_name = "log" 24 | end 25 | 26 | def self.vacuum( ) 27 | sql = "vacuum;" 28 | DBaccess.new().open do |db| 29 | db.execute( sql ) 30 | end 31 | end 32 | 33 | 34 | def self.write( db, str, level ) 35 | now = Time.now 36 | if level > 0 37 | sql = "insert into log (level,time,str) values (?,?,?);" 38 | if db == nil 39 | DBaccess.new().open do |db| 40 | db.execute( sql, level, now.to_i, str) 41 | end 42 | else 43 | db.execute( sql, level, now.to_i, str) 44 | end 45 | end 46 | txt = sprintf("%s: %s\n",now.strftime("%H:%M:%S"),str) 47 | File.open( LogFname, "a" ) do |fp| 48 | fp.puts( txt ) 49 | fp.flush 50 | end 51 | #STDOUT.puts( txt ) 52 | #STDOUT.flush 53 | end 54 | 55 | def self.sto( str ) 56 | write( nil, str, Stdout ) 57 | end 58 | 59 | def self.stoD( str ) # デバック時のみ出力 60 | if $debug == true 61 | write( nil, str, Stdout ) 62 | end 63 | end 64 | 65 | def self.debug( db, str ) 66 | write( db, str, Debug ) 67 | end 68 | 69 | def self.info( db, str ) 70 | write( db, str, Info ) 71 | end 72 | 73 | def self.atte( db, str ) 74 | write( db, str, Attention ) 75 | end 76 | 77 | def self.warn( db, str ) 78 | write( db, str, Warning ) 79 | end 80 | 81 | def self.error(db, str ) 82 | write( db, str, Error ) 83 | end 84 | 85 | def self.puts( *args ) 86 | if args.size == 3 87 | ( db, level, str ) = args 88 | elsif args.size == 2 89 | ( level, str ) = args 90 | db == nil 91 | elsif args.size == 1 92 | str = args.first 93 | level = Stdout 94 | else 95 | raise 96 | end 97 | 98 | now = Time.now 99 | if level > 0 100 | sql = "insert into log (level,time,str) values (?,?,?);" 101 | if db == nil 102 | DBaccess.new().open do |db| 103 | db.execute( sql, level, now.to_i, str) 104 | end 105 | else 106 | db.execute( sql, level, now.to_i, str) 107 | end 108 | end 109 | txt = sprintf("%s: %s\n",now.strftime("%H:%M:%S"),str) 110 | File.open( LogFname, "a" ) do |fp| 111 | fp.puts( txt ) 112 | end 113 | STDOUT.puts( txt ) 114 | end 115 | 116 | 117 | def select( db, level: nil, limit: nil ) 118 | where = [] 119 | args = [] 120 | sql = "select * from log " 121 | if level != nil 122 | where << "level > ?" 123 | args << level 124 | end 125 | 126 | if where.size > 0 127 | sql += " where " + where.join(" and ") 128 | end 129 | sql += " order by id desc " 130 | 131 | if limit != nil 132 | sql += limit 133 | end 134 | 135 | row = db.execute( sql, *args ) 136 | end 137 | 138 | # 139 | # カウント 140 | # 141 | def count( db, level: nil ) 142 | sql = "select count( id ) from log " 143 | argv = [] 144 | where = [] 145 | if level != nil 146 | where << "level > ?" 147 | argv << level 148 | end 149 | 150 | if where.size > 0 151 | sql += " where " + where.join(" and ") 152 | end 153 | 154 | row = db.execute( sql, *argv ) 155 | return row[0][0] 156 | end 157 | 158 | # 159 | # 削除 160 | # 161 | def deleteOld( db, time ) 162 | sql = "delete from log where time < ? ;" 163 | db.execute( sql, time ) 164 | end 165 | 166 | 167 | end 168 | -------------------------------------------------------------------------------- /src/db/Category.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class DBcategory 6 | 7 | include Base 8 | 9 | def initialize( ) 10 | @listL = { 11 | id: "id", 12 | name: "name", 13 | } 14 | @listM = { 15 | id: "id", 16 | pid: "pid", 17 | name: "name", 18 | } 19 | @paraL = [] 20 | @listL.each_pair { |k,v| @paraL << v } 21 | @paraM = [] 22 | @listM.each_pair { |k,v| @paraM << v } 23 | @changed = true 24 | @tbl_nameL = "categoryL" 25 | @tbl_nameM = "categoryM" 26 | end 27 | 28 | 29 | # 30 | # id から文字列に変換 31 | # 32 | def conv2str(db, l = nil, m = nil ) 33 | rl = rm = "" 34 | if l != nil and l != 0 35 | rl = selectL(db, id: l).first[:name] 36 | end 37 | if m != nil and m != 0 38 | rm = selectM(db, id: m).first[:name] 39 | end 40 | 41 | [ rl, rm ] 42 | end 43 | 44 | # 45 | # カテゴリの id 列(6-55 6-57 9-68) から文字列に変換 (大項目のみ) 46 | # 47 | def ids2str(db, ids ) 48 | cate = {} 49 | ids.split(/\s/).each do |tmp| 50 | l = m = 0 51 | if tmp =~ /(\d+)-(\d+)/ 52 | l,m = $1.to_i,$2.to_i 53 | elsif tmp =~ /(\d+)/ 54 | l,m = $1.to_i,nil 55 | end 56 | if l == 0 57 | cate = { "全て" => 1 } 58 | break 59 | else 60 | ls, m = conv2str(db, l, nil ) 61 | cate[ ls ] = 1 62 | end 63 | end 64 | return cate.keys.sort.join(", ") 65 | end 66 | 67 | 68 | # 69 | # json文字列から id に変換 70 | # 71 | def conv2id(db, data ) 72 | r = [] 73 | data.each_with_index do |cate,n| 74 | l = cate["large"]["ja_JP"] 75 | m = cate["middle"]["ja_JP"] 76 | tryc = lid = mid = 0 77 | begin 78 | raise if (lid = selectC(db, l: l )) == nil 79 | rescue 80 | insertL(db, l ) 81 | if tryc == 0 82 | tryc += 1 83 | retry 84 | else 85 | raise 86 | end 87 | end 88 | tryc = 0 89 | begin 90 | raise if (mid = selectC(db, l: lid, m: m )) == nil 91 | rescue 92 | insertM(db, lid, m ) 93 | if tryc == 0 94 | tryc += 1 95 | retry 96 | else 97 | raise 98 | end 99 | end 100 | r << [ lid,mid ] 101 | end 102 | r.sort! 103 | ( 3 - r.size).times {|n| r << [0,0] } 104 | r 105 | end 106 | 107 | # 108 | # キャッシュ作成 109 | # 110 | def createCache( db ) 111 | @main = {} 112 | @sub = {} 113 | sql = "select * from categoryL ;" 114 | row = db.execute( sql ) 115 | if row != nil and row.size != 0 116 | row.each do |tmp| 117 | @main[ tmp[1] ] = tmp[0] 118 | end 119 | end 120 | 121 | sql = "select * from categoryM ;" 122 | row = db.execute( sql ) 123 | if row != nil and row.size != 0 124 | row.each do |tmp| 125 | @sub[ tmp[1] ] ||= {} 126 | @sub[ tmp[1] ][ tmp[2] ] = tmp[0] 127 | end 128 | end 129 | @changed = false 130 | 131 | end 132 | 133 | def selectL(db, id: nil, name: nil) 134 | ( sql, args ) = makeSelectSql( @paraL, @tbl_nameL, id: id, name: name) 135 | row = db.execute( sql, args ) 136 | row2hash( @listL, row ) 137 | end 138 | 139 | def selectM(db, id: nil, pid: nil, name: nil) 140 | ( sql, args ) = makeSelectSql( @paraM, @tbl_nameM, id: id, pid: pid,name: name) 141 | row = db.execute( sql, args ) 142 | row2hash( @listM, row ) 143 | end 144 | 145 | def selectC(db, l: nil, m: nil) 146 | createCache( db ) if @changed == true 147 | if l != nil and m != nil 148 | if @sub[ l ] != nil 149 | return @sub[ l ][ m ] 150 | end 151 | elsif l != nil 152 | return @main[ l ] 153 | end 154 | nil 155 | end 156 | 157 | def insertL(db, cate ) 158 | sql = "insert into categoryL values (null, ? );" 159 | db.execute( sql, cate ) 160 | @changed = true 161 | end 162 | 163 | def insertM(db, pid, cate ) 164 | sql = "insert into categoryM values (null, ?, ? );" 165 | db.execute( sql, pid, cate, ) 166 | @changed = true 167 | end 168 | 169 | 170 | end 171 | -------------------------------------------------------------------------------- /src/views/fil_list.slim: -------------------------------------------------------------------------------- 1 | / 2 | / フィルター一覧 兼 自動予約一覧 3 | / 4 | 5 | css: 6 | #item { 7 | width:8em; 8 | } 9 | a.btn { 10 | margin: 1em 2em 2em 2em; 11 | } 12 | #button { 13 | margin: 0px 5px 0px 5px; 14 | } 15 | #cbcomm { 16 | color: #000000; 17 | } 18 | label { color: #000000} 19 | #title { 20 | margin: 0.5em 10em 0.5em 0.5em; 21 | font-size: 1.5em 22 | } 23 | #sort { 24 | margin: 0.5em 0.5em 0.5em 0.5em; 25 | padding-right: 2em ; 26 | background-color: #e1e2e3; 27 | } 28 | #rb { 29 | padding: 0em 2em 0em 1.6em; 30 | font-size: 1.3em 31 | } 32 | td,th { 33 | padding-top: 7px; 34 | padding-bottom: 7px; 35 | } 36 | 37 | - require_relative 'fil_list.rb' 38 | - dp = FilterList.new( @params, session ) 39 | 40 | 41 | .row 42 | .col 43 | // h1#title ==dp.printTitle() 44 | - if dp.type == FilConst::AutoRsv 45 | h1#title 自動予約一覧 46 | input type="hidden" id="url" value="/aut_rsv_list" 47 | - else 48 | h1#title フィルター一覧 49 | input type="hidden" id="url" value="/fil_list" 50 | 51 | .col#sort 52 | label 53 | span#rb 表示順 54 | label 55 | input name="stype" type="radio" value="title" checked=(dp.radioST[ARSort::Title]) 56 | span#rb タイトル 57 | label 58 | input name="stype" type="radio" value="reg" checked=(dp.radioST[ARSort::Reg]) 59 | span#rb 登録順 60 | label 61 | input name="stype" type="radio" value="cate" checked=(dp.radioST[ARSort::Cate]) 62 | span#rb ジャンル 63 | label 64 | input name="stype" type="radio" value="num" checked=(dp.radioST[ARSort::Num]) 65 | - if dp.type == FilConst::Filter 66 | span#rb 一致件数順 67 | - else 68 | span#rb 有効予約/一致件数順 69 | 70 | label 71 | input.cb#cb type="checkbox" name="reverse" checked=(dp.radioST[ARSort::Reverse]) 72 | span 逆順 73 | 74 | .row 75 | table.striped 76 | tr 77 | th No 78 | th タイトル or 検索文字列 79 | th ジャンル(大項目) 80 | - if dp.type == FilConst::AutoRsv 81 | th 有効予約/一致件数 82 | - else 83 | th 一致件数 84 | th アクション 85 | == dp.printTable() 86 | 87 | 88 | div id="sample-dialog" title="詳細" style="display:none;" 89 | p 削除しますか? 90 | 91 | 92 | javascript: 93 | $(".item").click(function(event){ 94 | //event.preventDefault(); 95 | var rid = $(this).attr('rid'); 96 | $("#sample-dialog").load('/fil_listD/' + rid ); 97 | $("#sample-dialog").dialog({ 98 | modal: true, 99 | maxWidth: 1200, 100 | width: 1000, 101 | buttons: { //ボタン 102 | "削除": function() { 103 | $.ajax({ 104 | url: '/fil_del/' + rid, 105 | type:"POST", 106 | async: true, 107 | timeout: 30000 108 | }).done( function(results) { 109 | location.reload(); 110 | }) 111 | }, 112 | "閉じる": function() { 113 | $(this).dialog("close"); 114 | } 115 | }, 116 | open: function() { 117 | $( this ).siblings('.ui-dialog-buttonpane').find('button:eq(2)').focus(); 118 | } 119 | }); 120 | return false; 121 | }); 122 | 123 | == Commlib::include( SrcDir + "/views/move_top.html" ) 124 | 125 | javascript: 126 | $(function(){ 127 | $( 'input[name="stype"]:radio' ).change( function() { 128 | var radioval = $(this).val(); 129 | var url = document.getElementById( "url" ).value ; 130 | var url2 = url + "?sort_type=" + radioval 131 | console.log(url2) 132 | window.location.href = url2 133 | }); 134 | }); 135 | 136 | $(function(){ 137 | $('.cb').on('change', function () { 138 | var url = document.getElementById( "url" ).value ; 139 | if ($(this).prop('checked')) { 140 | url = url + "?reverse=on"; 141 | } else { 142 | url = url + "?reverse=off"; 143 | } 144 | window.location.href = url ; 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /src/views/top.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 予約一覧 6 | # 7 | require 'sys/filesystem' 8 | 9 | class Top 10 | 11 | def initialize( ) 12 | $status = {} if $status == nil 13 | end 14 | 15 | def sec2str( sec ) 16 | if sec > 59 17 | s = sec % 60 18 | m = sec.to_i / 60 19 | if m > 59 20 | h = m.to_i / 60 21 | m = m % 60 22 | if m != 0 23 | return sprintf("%d時間 %d分",h,m) 24 | else 25 | return sprintf("%d時間",h ) 26 | end 27 | else 28 | return sprintf("%d分",m) 29 | end 30 | else 31 | return sprintf("%d秒",sec) 32 | end 33 | end 34 | 35 | def setup() 36 | r = {} 37 | 38 | # Disk情報 39 | stat = Sys::Filesystem.stat( DataDir ) 40 | diskTotal = (stat.blocks * stat.block_size).to_f 41 | diskFree = (stat.blocks_available * stat.block_size).to_f 42 | used = diskTotal - diskFree 43 | r[:diskTotal] = sprintf("%.1f GB", diskTotal / Const::GB ) 44 | r[:diskFree] = sprintf("%.1f GB", diskFree / Const::GB ) 45 | r[:diskUsed] = sprintf("%.1f GB", used / Const::GB ) 46 | wari = 100.0 * used / diskTotal 47 | r[:diskWari] = sprintf("%.1f %%", wari ) 48 | 49 | # 予約情報 50 | now = Time.now.to_i 51 | reserve = DBreserve.new 52 | reserveNum = 0 # 予約数 53 | nowRecTitle = [] # 録画中のタイトル 54 | nextRecTime = nil # 次の予約時間 55 | nextRecTitle = "" # 次番組のタイトル 56 | conflictNum = 0 # 競合中の数 57 | epg = 0 # EPG取得中か 58 | remainingTime = 0 # 録画残り時間 59 | stat2 = 0 60 | errNum = 0 # 録画エラー数 61 | 62 | DBaccess.new().open do |db| 63 | row = reserve.select( db, tend: now, order: "order by start" ) 64 | reserveNum = row.size 65 | row.each do |r| 66 | 67 | case r[:stat] 68 | when RsvConst::Normal then 69 | start2 = r[:start] - Start_margin 70 | if nextRecTime == nil or start2 < nextRecTime 71 | nextRecTime = start2 72 | nextRecTitle = r[:title] 73 | end 74 | when RsvConst::RecNow then 75 | nowRecTitle << r[:title] 76 | remainingTime = r[:end] if remainingTime < r[:end] 77 | when RsvConst::Conflict then 78 | conflictNum += 1 79 | end 80 | end 81 | 82 | point = now - ( 3 * 24 * 3600 ) 83 | row = reserve.select( db, tend: point, order: "order by start" ) 84 | reserveNum = row.size 85 | row.each do |r| 86 | errNum += 1 if r[:stat] == RsvConst::AbNormalEnd 87 | if r[:dropNum ] != nil 88 | ( drer, pcr, execerror ) = reserve.parseDropNum( r[:dropNum ] ) 89 | errNum += 1 if drer > PacketChk_threshold 90 | end 91 | end 92 | 93 | stat2 = DBkeyval.new.select(db,StatConst::KeyName ) 94 | end 95 | 96 | r[:stat2] = "" 97 | if nowRecTitle.size > 0 98 | r[:stat] = sprintf("録画中 ( 残り %s )", 99 | sec2str( remainingTime - Time.now.to_i )) 100 | r[:stat2] = nowRecTitle.join("
") 101 | else 102 | if test( ?f, EPGLockFN ) 103 | r[:stat] = case stat2 104 | when StatConst::FileCopy then "ファイル転送中" 105 | when StatConst::PacketChk then "ファイルチェック中" 106 | else "EPG取得中" 107 | end 108 | elsif reserveNum == 0 or nextRecTime == nil 109 | r[:stat] = "予約待ち" 110 | elsif nextRecTime > 0 111 | t = sec2str( nextRecTime - now ) 112 | r[:stat] = sprintf("待機中 ( 開始まで %s )",t ) 113 | r[:stat2] = nextRecTitle 114 | else 115 | r[:stat] = "-" 116 | end 117 | end 118 | if conflictNum > 0 119 | r[:ConflictNum] = %Q( #{conflictNum} ) 120 | else 121 | r[:ConflictNum] = conflictNum 122 | end 123 | 124 | r[:reserveNum] = reserveNum 125 | 126 | # 過去のエラー数 127 | if errNum > 0 128 | r[:ErrorNum] = %Q( #{errNum} ) 129 | else 130 | r[:ErrorNum] = errNum 131 | end 132 | 133 | r 134 | end 135 | 136 | end 137 | 138 | -------------------------------------------------------------------------------- /src/model/DelayedEPG.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # 4 | # 録画済みの TSファイルから、EPG データを採取する 5 | # 6 | # ・32bit OS のRaspbian で 大きいファイルの最後に seek しようとすると失敗 7 | # するので、最初の 1Gbyte で。 8 | # ・それでも epgdump が coreダンプするものがあり、録画した TS から抽出 9 | # するのは無理か 10 | 11 | class DelayedEPG 12 | 13 | def initialize( time_limit ) 14 | 15 | @ts = Time.now 16 | phchid = DBphchid.new 17 | reserve = DBreserve.new 18 | 19 | list = [] 20 | chid2upt = {} # chid 対 更新時間 21 | chid2phch = {} # chid 対 phch 22 | 23 | DBaccess.new().open( tran: true ) do |db| 24 | 25 | # EPG取得 の最終実施時間の取得 26 | ph = phchid.select(db) 27 | ph.each do |tmp| 28 | chid2upt[ tmp[:chid] ] = tmp[:updatetime] 29 | chid2phch[ tmp[:chid] ] = tmp[:phch] 30 | end 31 | #pp chid2upt 32 | 33 | # 最近の録画リスト取得 34 | order = "order by end desc" 35 | row = reserve.select( db, stat: RsvConst::NormalEnd, order: order, limit: "10" ) 36 | exist = {} # 重複 37 | row.each do |tmp| 38 | #pp "+ #{tmp[:title]} #{Time.at(tmp[:end]).to_s}" 39 | chid = tmp[:chid] 40 | et = tmp[:end] 41 | if chid2upt[chid] < et 42 | #pp et - chid2upt[chid] 43 | path = makePath( tmp ) 44 | if test(?f, path ) 45 | if exist[ chid ] == nil 46 | list << tmp 47 | exist[ chid ] = true 48 | end 49 | else 50 | pp "file not found #{path}" 51 | end 52 | end 53 | end 54 | end 55 | 56 | return if list.size == 0 57 | 58 | ge = GetEPG.new 59 | count = { :upd => 0, :ins => 0, :del => 0, :same => 0 } 60 | 61 | list.reverse.each do |tmp| # 処理は古い方から 62 | 63 | # 中断の判断 64 | if Time.now > time_limit 65 | DBlog::sto("DelayedEPG time limit: #{timeLimit}" ) 66 | return 67 | end 68 | 69 | tspath = makePath( tmp ) 70 | pp "#{tmp[:title]} #{tspath} #{tmp[:chid]}" 71 | json = JsonDir + "/#{tmp[:chid]}.json" 72 | phch = chid2phch[ tmp[:chid] ] 73 | next if phch == nil 74 | 75 | @ts = Time.now 76 | ppp("execEpgdump() start") 77 | execEpgdump( tspath, json ) 78 | if test(?f, json ) and File.size( json ) > 100 79 | ge.readJsonProc( json, phch, count ) 80 | else 81 | DBlog::sto( "json file error (#{json})" ) 82 | end 83 | ppp("execEpgdump() end") 84 | end 85 | 86 | str = sprintf("ins=>%4d,upd=>%4d,same=>%4d", 87 | count[:ins],count[:upd],count[:same] ) 88 | DBlog::debug(nil, "delayed EPG取得終了 #{str}" ) 89 | 90 | end 91 | 92 | def makePath( tmp ) 93 | subd = tmp[:subdir] == "" ? "" : "/" + Commlib::normStr(tmp[:subdir]) 94 | path = sprintf("%s%s/%s",TSDir, subd, Commlib::normStr(tmp[:fname]) ) 95 | end 96 | 97 | def execEpgdump( tspath, json ) 98 | bsize = 1024 99 | outbuf = "x" * bsize; 100 | cmd = [ Epgdump, "json", "-", json, :err=>[:child, :out] ] 101 | limit = 1024 * 1024 * 1024 # 1Gbyte 102 | fsize = 0 103 | st = Time.now 104 | 105 | IO.popen( cmd, "r+" ) do |io| 106 | File.open( tspath, "r") do |fr| 107 | begin 108 | while fr.sysread( bsize, outbuf ) != nil 109 | io.write( outbuf ) 110 | fsize += outbuf.size 111 | break if fsize > limit 112 | break if ( Time.now - st ) > 120 # 120秒で timeout 113 | end 114 | rescue EOFError => e 115 | rescue => e 116 | pp e 117 | end 118 | io.close_write 119 | end 120 | end 121 | end 122 | 123 | end 124 | 125 | 126 | if File.basename($0) == "DelayedEPG.rb" 127 | base = File.dirname( $0 ) 128 | [ ".", "..","src", base ].each do |dir| 129 | if test( ?f, dir + "/require.rb") 130 | $: << dir 131 | $baseDir = dir 132 | end 133 | end 134 | 135 | require 'require.rb' 136 | 137 | phchid = DBphchid.new 138 | DBaccess.new().open( tran: true ) do |db| 139 | phchid.add(db, "BS15_0","BS_200", (Time.now - 3600 * 24 ).to_i ) 140 | end 141 | 142 | $debug = true 143 | 144 | DelayedEPG.new( Time.now + 600 ) 145 | 146 | exit 147 | 148 | end 149 | -------------------------------------------------------------------------------- /src/views/fil_res_dsp.js: -------------------------------------------------------------------------------- 1 | 2 | 121 | -------------------------------------------------------------------------------- /src/TV/Video.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # 4 | # raspirecTV.rb 関連 5 | # 6 | 7 | class Video 8 | 9 | attr_reader :recPid 10 | 11 | def initialize( serial = 0 ) 12 | @port = UDPbasePort + serial 13 | if RemoteMonitor == true 14 | @videoPort = "udp://#{RecHostName}:#{@port}/" 15 | else 16 | @videoPort = "udp://localhost:#{@port}/" 17 | end 18 | @ipc = makeTmpFn( serial, "cmd" ) # mpv のコマンド入力 19 | @recPid = nil 20 | @mpvpid = nil 21 | @serial = serial 22 | @nullP = File.open( "/dev/null","w+") 23 | end 24 | 25 | # 26 | # 中間(作業)ファイル名の作成 27 | # 28 | def makeTmpFn( serial, key = "fifo" ) 29 | fn = sprintf("/tmp/raspirecTV_%03d.%s",serial, key) 30 | return fn 31 | end 32 | 33 | # 34 | # socket にコマンド書き込み 35 | # 36 | def ipcSendMsg( fname, msg ) 37 | dlog( msg ) 38 | if RemoteMonitor == false 39 | s = UNIXSocket.new(fname) 40 | s.write(msg + "\n" ) 41 | s.close 42 | else 43 | cmd = %W( ssh #{XServerName} socat - #{fname} ) 44 | dlog( cmd.join(" ") ) 45 | Open3.popen3( *cmd ) do | stdin, o, e, t| 46 | stdin.puts( msg ) 47 | stdin.close 48 | end 49 | end 50 | end 51 | 52 | # 53 | # 停止 54 | # 55 | def stop( ) 56 | [ @mpvPid, @recPid ].each do |pid| 57 | if pid != nil 58 | begin 59 | Process.kill( :KILL, pid ) 60 | Process.waitpid( pid ) 61 | rescue 62 | end 63 | end 64 | end 65 | @mpvPid = @recPid = nil 66 | 67 | File.unlink( @ipc ) if FileTest.socket?( @ipc ) 68 | end 69 | 70 | 71 | # 72 | # チャンネル変更 73 | # 74 | def chChange( chinfo, devfn, pageName ) 75 | 76 | return false if @recPid == nil 77 | begin 78 | Process.kill( :HUP, @recPid ) 79 | rescue 80 | end 81 | @recPid = nil 82 | sepT = RaspirecTV_GAPT 83 | sleep( sepT ) 84 | th = Thread.new() do 85 | cmd = makeRecCmd( chinfo, devfn ) 86 | dlog( cmd.join(" ") ) 87 | opt = {} 88 | if $arg.d < 2 89 | opt = { :out => @nullP, :err => @nullP } 90 | end 91 | st = Time.now 92 | begin 93 | @recPid = spawn( *cmd, opt ) 94 | Process.waitpid( @recPid ) 95 | rescue 96 | end 97 | sa = Time.now - st 98 | if sa < 1.0 99 | msg = "#{Recpt1_cmd} の起動に失敗しました。" 100 | $queue.push($event.new(:msg, nil, msg )) 101 | end 102 | end 103 | sleep( sepT ) 104 | ipcSendMsg( @ipc, "loadfile #{@videoPort}" ) 105 | sleep( 3 ) 106 | tmp = sprintf("\"%s : %s\"" ,pageName, chinfo.chname ) 107 | ipcSendMsg( @ipc, "set title #{tmp}" ) 108 | return true 109 | end 110 | 111 | 112 | 113 | # 114 | # recpt1/recdvb のコマンド組み立て 115 | # 116 | def makeRecCmd( chinfo, devfn ) 117 | 118 | ch = chinfo.phch 119 | sid = chinfo.band == Const::GR ? "hd" : chinfo.svid 120 | dev = devfn 121 | time = 9999 122 | udp = true 123 | port = @port 124 | addr = RemoteMonitor == false ? "localhost" : XServerName 125 | 126 | pt1 = Recpt1.new 127 | ret = pt1.makeCmd( ch,time,b25: true,sid: sid, dev: dev,udp: udp, port: port, addr: addr ) 128 | 129 | return ret 130 | end 131 | 132 | # 133 | # recdvb を起動 134 | # 135 | def play( chinfo, tun ) 136 | 137 | reccmd = makeRecCmd( chinfo, tun.devfn ) 138 | mpvcmd = [] 139 | if RemoteMonitor == true 140 | mpvcmd = %W( ssh -t -t #{XServerName} env DISPLAY=:0 ) 141 | end 142 | title = sprintf("--title=\"%s : %s\"" ,tun.name, chinfo.chname ) 143 | mpvcmd << Mpv_cmd 144 | mpvcmd += Mpv_opt + [ "--no-terminal", 145 | title, 146 | "--input-ipc-server=" + @ipc, 147 | @videoPort 148 | ] 149 | opt = {} 150 | if $arg.d < 2 151 | opt = { :out => @nullP, :err => @nullP } 152 | end 153 | 154 | dlog( mpvcmd.join(" ")) 155 | @mpvPid = spawn( *mpvcmd, opt) # exec mpv 156 | 157 | dlog( reccmd.join(" ")) 158 | @recPid = spawn( *reccmd, opt ) # exec recpt1/dvb 159 | 160 | Thread.new(@recPid) do |pid| 161 | begin 162 | Process.waitpid( pid ) 163 | rescue 164 | end 165 | end 166 | end 167 | 168 | end 169 | -------------------------------------------------------------------------------- /src/views/rsv_tbl.slim: -------------------------------------------------------------------------------- 1 | 2 | css: 3 | label { color: #000000} 4 | 5 | - require_relative 'rsv_tbl.rb' 6 | - pt = RsvTbl.new( @day, @time ) 7 | - timeHref = pt.getTimeHref() 8 | 9 | 10 | .row 11 | .col 12 | .div 13 | == pt.dateSel() 14 | .col 15 | a.btn.waves-effect.waves-light href="#{timeHref[0]}" 朝 16 | .col 17 | a.btn.waves-effect.waves-light href="#{timeHref[1]}" 昼 18 | .col 19 | a.btn.waves-effect.waves-light href="#{timeHref[2]}" 夜 20 | .col 21 | a.btn.waves-effect.waves-light href="#{timeHref[3]}" 深夜 22 | .col 23 | a.btn.waves-effect.waves-light.btn-small.lime.darken-4 href="#{timeHref[4]}" -3H 24 | .col 25 | a.btn.waves-effect.waves-light.btn-small.lime.darken-4 href="#{timeHref[5]}" +3H 26 | 27 | .row 28 | div style="display:table;border-collapse: separate;border-spacing: 3px 0;" 29 | .dtr 30 | div.station style="width:2.3em;" 31 | a href="/cate_color" style="color:white;" 色 32 | == pt.yokojiku() 33 | .dtr 34 | .dtc 35 | == pt.tatejiku() 36 | == pt.prgTable() 37 | 38 | == pt.nowLine() 39 | 40 | 41 | div id="sample-dialog" title="詳細" style="display:none;" 42 | p 43 | 44 | == Commlib::include( SrcDir + "/views/move_top.html" ) 45 | == Commlib::include( SrcDir + "/views/tooltip.js" ) 46 | 47 | css: 48 | .inline-block { 49 | display: inline-block; /* インラインブロック要素にする */ 50 | /* background-color:#66cc33; */ 51 | padding: 0px; 52 | margin: 0px 2px 0px 2px; 53 | } 54 | div.dtr { 55 | display:table-row; 56 | } 57 | 58 | div.dtc { 59 | display:table-cell; 60 | } 61 | 62 | div.item { 63 | margin: 0px 0px 0px 0px; 64 | padding: 0px; 65 | width: 160px; 66 | opacity: 0.9; 67 | overflow:hidden; 68 | } 69 | 70 | div.time { 71 | height: 176px; 72 | margin: 4px; 73 | padding: 5px; 74 | color: white; 75 | background-color:#333; 76 | width: 2em; 77 | } 78 | div.station { 79 | width:160px; 80 | margin: 0px; 81 | padding: 5px; 82 | border: none; 83 | color: white; 84 | background-color:#333; 85 | display: table-cell; 86 | } 87 | 88 | .ui-dialog-buttonset { 89 | width: 100%; 90 | display: flex; 91 | display: -webkit-flex; 92 | text-align: center; 93 | justify-content: space-around; 94 | -webkit-justify-content: space-around; 95 | } 96 | 97 | javascript: 98 | $('.dropdown-trigger').dropdown(); 99 | 100 | $(document).ready(function(){ $('.tabs').tabs(); }); 101 | 102 | $(document).ready(function(){ $('select').formSelect(); }); 103 | 104 | $(".item").hover(function() { 105 | $(this).css('border','solid #a23456 1px'); 106 | }, function() { 107 | $(this).css('border',''); 108 | }); 109 | 110 | 111 | $(".item").click(function(event){ 112 | var rid = $(this).attr('rid'); 113 | //event.preventDefault(); 114 | $("#sample-dialog").load('/rsv_list_D/' + rid ); 115 | $("#sample-dialog").dialog({ 116 | modal: true, 117 | maxWidth: 1200, 118 | width: 1000, 119 | buttons: { //ボタン 120 | "修正": function() { 121 | $.ajax({ 122 | url: '/rsv_list/Mod/' + rid, 123 | type:"POST", 124 | data: $('form').serialize() + '¶m=1', 125 | async: true, 126 | timeout: 30000 127 | }).done( function(results) { 128 | location.reload(); 129 | }) 130 | }, 131 | "削除": function() { 132 | $.ajax({ 133 | url: '/rsv_list/Del/' + rid, 134 | type:"POST", 135 | async: true, 136 | timeout: 30000 137 | }).done( function(results) { 138 | location.reload(); 139 | }) 140 | }, 141 | "閉じる": function() { 142 | $(this).dialog("close"); 143 | } 144 | }, 145 | open: function() { 146 | $( this ).siblings('.ui-dialog-buttonpane').find('button:eq(2)').focus(); 147 | } 148 | }); 149 | return false; 150 | }); 151 | -------------------------------------------------------------------------------- /src/views/layout.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | title = @title 6 | meta http-equiv='Content-Type' content='text/html' charset='utf-8' 7 | meta name= 'viewport' content='width=device-width, initial-scale=1.0' 8 | 9 | body 10 | - if Local_jquery == false 11 | script src='https://code.jquery.com/jquery-3.3.1.min.js' 12 | script src='https://code.jquery.com/ui/1.12.0/jquery-ui.min.js' 13 | link rel='stylesheet' href='http://code.jquery.com/ui/1.12.1/themes/pepper-grinder/jquery-ui.css' 14 | 15 | link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" 16 | script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js" 17 | 18 | - if Local_jquery == true 19 | script src='/jquery-3.3.1.min.js' 20 | script src='/jquery-ui.min.js' 21 | link rel='stylesheet' href='/jquery-ui.css' 22 | link rel='stylesheet' href='/materialize.min.css' media='screen,projection' 23 | script src='/materialize.min.js' 24 | 25 | link rel='stylesheet' href='/style.css' 26 | link rel='stylesheet' href='/overlaid.css' 27 | link rel='stylesheet' href='/nav.css' 28 | 29 | div.navbar-fixed 30 | nav 31 | div.nav-wrapper 32 | /! .yellow.darken-3 33 | a.right.brand-logo href='/' 34 | | raspirec 35 | a.sidenav-trigger.top-nav href="#!" data-target="mobile-demo" 36 | i.material-icons menu 37 | ul.left.hide-on-med-and-down#nav-mobile 38 | li 39 | a.dropdown-trigger href="#!" data-target="DD0" style="padding-left:3em;padding-right:3em;" 番組表 40 | li 41 | a.dropdown-trigger href="#!" data-target="DD1" style="padding-left:4em;padding-right:4em;" 予約 42 | li 43 | a.dropdown-trigger href="#!" data-target="DD2" style="padding-left:5em;padding-right:4em;" その他 44 | 45 | ul.dropdown-content id="DD0" style="padding-left:0em;padding-right:0em;" 46 | li 47 | a href='/prg_tbl/GR0' 地デジ 番組表 48 | - if ( BSCS_tuner_num + GBC_tuner_num ) > 0 49 | li 50 | a href='/prg_tbl/BS0' BS 番組表 51 | li 52 | a href='/prg_tbl/CS0' CS 番組表 53 | li 54 | a href='/fil_list' フィルター 55 | li 56 | a href='/ch_tbl_list' 放送局一覧 57 | 58 | ul.dropdown-content id="DD1" style="padding-left:0em;padding-right:0em;" 59 | li 60 | a href='/rsv_list' 予約一覧 61 | li 62 | a href='/rsv_tbl' 予約状況表 63 | li 64 | a href='/rsv_list_old' 録画済み一覧 65 | li 66 | a href='/aut_rsv_list' 自動予約 67 | 68 | ul.dropdown-content id="DD2" style="padding-left:0em;padding-right:0em;" 69 | li 70 | a href='/search' 番組検索 71 | li 72 | a href='/log_view' ログ 73 | 74 | - if MonitorFunc == true 75 | li 76 | a href='/monitor' hls モニター 77 | 78 | - if MPMonitor == true 79 | li 80 | a href='/mpv_mon' mpv モニター 81 | li 82 | a href='/control' コントロールパネル 83 | 84 | - if Debug == true 85 | li 86 | a href='/kill' restart httpd 87 | li 88 | a href='/kill2' restart http,timer 89 | 90 | ul.sidenav#mobile-demo 91 | li 92 | a href='/prg_tbl/GR0' 地デジ 番組表 93 | - if ( BSCS_tuner_num + GBC_tuner_num ) > 0 94 | li 95 | a href='/prg_tbl/BS0' BS 番組表 96 | li 97 | a href='/prg_tbl/CS0' CS 番組表 98 | li 99 | a href='/fil_list' フィルター 100 | li 101 | a href='/ch_tbl_list' 放送局一覧 102 | li 103 | a href='/rsv_list' 予約一覧 104 | li 105 | a href='/rsv_tbl' 予約状況表 106 | li 107 | a href='/rsv_list_old' 録画済み一覧 108 | li 109 | a href='/aut_rsv_list' 自動予約 110 | li 111 | a href='/search' 番組検索 112 | 113 | - if MonitorFunc == true 114 | li 115 | a href='/monitor' hls モニター 116 | 117 | - if MPMonitor == true 118 | li 119 | a href='/mpv_mon' mpv モニター 120 | li 121 | a href='/log_view' ログ 122 | li 123 | a href='/control' コントロールパネル 124 | p 125 | 126 | == yield 127 | javascript: 128 | $(".dropdown-trigger").dropdown(); 129 | $(document).ready(function(){ 130 | $('.sidenav').sidenav(); 131 | }); 132 | 133 | -------------------------------------------------------------------------------- /src/tool/reserve_SL.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 予約関係データの save & load 6 | # 7 | 8 | require 'optparse' 9 | require 'yaml' 10 | 11 | base = File.dirname( $0 ) 12 | [ ".", "..", "src",base + "/.."].each do |dir| 13 | if test(?f,dir + "/require.rb") 14 | $: << dir 15 | end 16 | end 17 | require 'require.rb' 18 | 19 | 20 | $opt = { 21 | :mode => :save, # save or load 22 | :fname => nil, # output/input file 23 | :o => false, # 過去の予約データ 24 | :f => false, # フィルター only 25 | :r => false, # 予約データ 26 | :a => false, # 自動予約データ 27 | :C => false, # データ削除 28 | :db => DbFname, # DB ファイル 29 | } 30 | $pname = File.basename( $0 ) 31 | $version = "1.1.0" 32 | 33 | def usage() 34 | usageStr = <<"EOM" 35 | Usage: #{$pname} [Options1] [Options2...] file 36 | 37 | Options1: 38 | -s, --save save モード(デフォルト) 39 | -l, --load load モード 40 | 41 | Options2: 42 | -f, --filter フィルターデータ 43 | -a, --auto 自動予約データ 44 | -r, --reserv 未録画 予約データ 45 | -o, --old 録画済み 予約データ 46 | -A, --ALL 全部(-f,-a,-r,-o) 47 | -d, --db db_file DBファイルの指定(デフォルトは config.rb 中の DbFname ) 48 | -C, --clearTable 読み込む前にデータ削除 49 | 50 | file 入力/出力 ファイル名 51 | 52 | #{$pname} ver #{$version} 53 | EOM 54 | puts usageStr 55 | exit 1 56 | end 57 | 58 | OptionParser.new do |opt| 59 | opt.on('-s','--save') { $opt[:mode] = :save } 60 | opt.on('-l','--load') { $opt[:mode] = :load } 61 | opt.on('-d f','--db f') {|v| $opt[:db] = v } 62 | 63 | opt.on('-f','--filter') { $opt[:f] = true } 64 | opt.on('-a','--auto') { $opt[:a] = true } 65 | opt.on('-r','--reserv') { $opt[:r] = true } 66 | opt.on('-o','--old') { $opt[:o] = true } 67 | opt.on('-A','--ALL') { $opt[:f] = $opt[:a] = $opt[:r] = $opt[:o] = true;} 68 | opt.on('-C','--clearTable') { $opt[:C] = true; } 69 | opt.parse!(ARGV) 70 | end 71 | 72 | if ARGV.size == 1 73 | $opt[:fname] = ARGV[0] 74 | else 75 | usage() 76 | end 77 | 78 | reserve = DBreserve.new 79 | filter = DBfilter.new 80 | data = {} 81 | now = Time.now.to_i 82 | 83 | if $opt[:mode] == :save 84 | 85 | data[:fil] ||= [] 86 | data[:auto] ||= [] 87 | data[:rsv] ||= [] 88 | data[:old] ||= [] 89 | 90 | DBaccess.new( $opt[:db] ).open do |db| 91 | row = filter.select( db ) 92 | row.each do |r| 93 | if $opt[:f] == true and r[:type] == 0 94 | data[:fil] << r 95 | end 96 | if $opt[:a] == true and r[:type] == 1 97 | data[:auto] << r 98 | end 99 | end 100 | 101 | row = reserve.select( db ) 102 | row.each do |r| 103 | if $opt[:o] == true and r[:stat] > 1 104 | data[:old] << r 105 | elsif $opt[:r] == true and r[:stat] < 2 106 | data[:rsv] << r 107 | end 108 | end 109 | end 110 | 111 | str = YAML.dump(data) 112 | if $opt[:fname] != nil and $opt[:fname] != "-" 113 | File.open( $opt[:fname], "w" ) do |fp| 114 | fp.puts(str) 115 | end 116 | else 117 | STDOUT.puts(str) 118 | end 119 | 120 | elsif $opt[:mode] == :load 121 | 122 | if $opt[:fname] != nil and $opt[:fname] != "-" 123 | File.open( $opt[:fname], "r" ) do |fp| 124 | str = fp.read() 125 | end 126 | else 127 | str = STDIN.read() 128 | end 129 | data = YamlWrap.load( str ) 130 | 131 | DBaccess.new( $opt[:db] ).open( tran: true ) do |db| 132 | 133 | if $opt[:C] == true 134 | filter.select( db ).each do |r| 135 | if ( $opt[:a] == true and r[:type] == 1 ) or 136 | ( $opt[:f] == true and r[:type] == 0 ) 137 | filter.delete( db, r[:id] ) 138 | end 139 | end 140 | reserve.select( db ).each do |r| 141 | if ( $opt[:o] == true and r[:stat] > 1 ) or 142 | ( $opt[:r] == true and r[:stat] < 2 ) 143 | reserve.delete( db, id: r[:id] ) 144 | end 145 | end 146 | end 147 | 148 | if $opt[:f] == true 149 | data[:fil].each do |r| 150 | r[:id] = nil 151 | filter.insert( db, r ) 152 | end 153 | end 154 | if $opt[:a] == true 155 | data[:auto].each do |r| 156 | r[:id] = nil 157 | filter.insert( db, r ) 158 | end 159 | end 160 | 161 | if $opt[:o] == true 162 | data[:old].each do |r| 163 | r[:id] = nil 164 | reserve.insert( db, r ) 165 | end 166 | end 167 | if $opt[:r] == true 168 | data[:rsv].each do |r| 169 | r[:id] = nil 170 | reserve.insert( db, r ) 171 | end 172 | end 173 | end 174 | 175 | end 176 | -------------------------------------------------------------------------------- /src/views/rsv_list_D.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # 予約一覧 詳細ダイアログ 6 | # 7 | 8 | 9 | class ReservationListD 10 | 11 | def initialize( ) 12 | end 13 | 14 | def getData( id ) 15 | DBaccess.new().open do |db| 16 | programs = DBprograms.new() 17 | reserve = DBreserve.new 18 | data = reserve.select( db, id: id ) 19 | if data.size > 0 20 | t1 = data.first 21 | t2 = programs.selectSP( db, evid: t1[:evid], chid: t1[:chid] ) 22 | t3 = t2.first 23 | if t3 != nil 24 | t3.each_pair do |k,v| 25 | t1[k] = v 26 | end 27 | end 28 | return t1 29 | end 30 | end 31 | nil 32 | end 33 | 34 | 35 | 36 | def switch( name, init = 0, onoff = nil ) 37 | on = "On" 38 | off = "Off" 39 | if onoff != nil 40 | on = onoff[0] 41 | off = onoff[1] 42 | end 43 | 44 | str = < 46 |
47 | 53 |
54 | EOS 55 | str 56 | end 57 | 58 | def checkbox( name, comment, val ) 59 | checked = "" 60 | if val == 1 61 | checked = "checked=\"checked\"" 62 | end 63 | str = < 65 | 66 | #{comment} 67 | 68 | EOS 69 | str 70 | end 71 | 72 | def inputText( name, comment: "", val: "", size: 60, dlist: nil ) 73 | dl = "" 74 | if dlist != nil and dlist.class == Array 75 | tmp = [] 76 | tmp << %q( ) 77 | tmp << dlist.map{|item| sprintf(" ) 79 | dl = tmp.join("\n") 80 | end 81 | 82 | str = < 85 | 86 | #{dl} 87 | 88 | EOS 89 | str 90 | end 91 | 92 | def printTable( rid ) 93 | if ( t = getData( rid )) != nil 94 | printTable2( [t,nil] ) 95 | end 96 | end 97 | 98 | 99 | # 100 | # 自動録画用のオプション項目の出力 101 | # 102 | def printOpt( dp, use_use: true ) 103 | 104 | dirs = Commlib::datalist_dir() 105 | 106 | chk = %q(checked="checked") 107 | jitanchk = dp[:jitan] == 0 ? chk : "" 108 | usechk = ( dp[:stat] == RsvConst::NotUse or dp[:stat] == RsvConst::NotUseA ) ? chk : "" 109 | subdir = dp[:subdir] 110 | 111 | str1 = < 113 | 保存サブディレクトリ 114 | 115 | 116 | #{dirs} 117 | 118 | 119 |
120 | 124 | EOS 125 | str2 = < 127 | 131 | EOS 132 | if use_use != true 133 | return str1 134 | else 135 | return str1 + str2 136 | end 137 | end 138 | 139 | # 140 | # データの表示 141 | # 142 | def printTable2( data, use_use = true ) 143 | r = [] 144 | t = data[0] 145 | t[:date] = Commlib::stet_to_s( t[:start], t[:end] ).join(" ") 146 | 147 | opt = printOpt( t, use_use: use_use ) 148 | name = t[:name] == nil ? "" : t[:name] 149 | detail = t[:detail] == nil ? "" : t[:detail] 150 | 151 | [["放送局名", name + " " + "(#{t[:chid]})" ], 152 | ["番組名", t[:title] + " " + "(evid=#{t[:evid]})" ], 153 | ["内容", detail ], 154 | ["録画時間", t[:date] ], 155 | ["種別", t[:type] == 0 ? "手動予約" : "自動予約"], 156 | ["オプション", opt ], 157 | ].each do |tmp| 158 | r << printTable3(tmp[0], tmp[1]) 159 | end 160 | 161 | if t[:stat] == RsvConst::RecNow 162 | r << Commlib::print_hidden( id: "recFlag", name: "RecNow", val: "1" ) 163 | end 164 | r.join("\n") 165 | end 166 | 167 | def printTable3( title, val) 168 | sprintf(%Q{ %s %s },title, val) 169 | end 170 | 171 | end 172 | 173 | if File.basename($0) == "rsv_list_D.rb" 174 | 175 | [ ".", ".." ].each do |dir| 176 | if test(?f,dir + "/require.rb") 177 | $: << dir 178 | end 179 | end 180 | require 'require.rb' 181 | 182 | rt = ReservationListD.new() 183 | rt.printTable( 4 ) 184 | 185 | exit 186 | 187 | case ARGV[0] 188 | when "1" 189 | puts( rt.printTable( 1 ) ) 190 | end 191 | 192 | end 193 | -------------------------------------------------------------------------------- /src/db/base.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | module Base 6 | 7 | # 8 | # select文の生成 9 | # 10 | def makeSelectSql(para, tname, 11 | id: nil, 12 | pid: nil, 13 | type: nil, 14 | svid: nil, 15 | evid: nil, 16 | chid: nil, 17 | order: nil, 18 | name: nil, 19 | tstart: nil, 20 | tend: nil, 21 | limit: nil, 22 | keyid: nil, 23 | updatetime: nil, 24 | skip: nil, 25 | stat: nil, 26 | recpt1pid: nil 27 | ) 28 | where = [] 29 | args = [] 30 | sql = "select " + para.join(",") + " from #{tname} " 31 | if id != nil 32 | where << " id = ? " 33 | args << id 34 | elsif pid != nil 35 | where << " pid = ? " 36 | args << pid 37 | elsif type != nil 38 | where << " type = ? " 39 | args << type 40 | elsif name != nil 41 | where << " name = ? " 42 | args << name 43 | elsif chid != nil and evid != nil 44 | where << " chid = ? and evid = ? " 45 | args += [ chid, evid ] 46 | elsif chid != nil 47 | where << " chid = ? " 48 | args << chid 49 | elsif keyid != nil 50 | where << " keyid = ? " 51 | args << keyid 52 | elsif updatetime != nil 53 | where << " updatetime < ? " 54 | args << updatetime 55 | elsif evid != nil 56 | where << " evid = ? " 57 | args << evid 58 | # else # デバック用 59 | # if tstart == nil and tend == nil and stat == nil 60 | # DBlog::sto( "Error: makeSelectSql()" ) 61 | # if $debug == true 62 | # DBlog::sto( caller(0)[0..9].join("\n" )) 63 | # end 64 | # end 65 | end 66 | 67 | # ## デバック用 68 | # found = [] 69 | # %W( id pid type name chid keyid updatetime evid tstart tend ).each do |n| 70 | # r = if eval( "defined? #{n}" ) 71 | # eval( n ) 72 | # else 73 | # nil 74 | # end 75 | # found << n if r != nil 76 | # end 77 | # if found.size > 1 78 | # tmp = found.join(",") 79 | # if tmp != "chid,evid" and 80 | # tmp != "chid,evid,tstart" and 81 | # tmp != "id,type" and 82 | # tmp = sprintf("@@@: makeSelectSql %d %s\n%s\n",found.size, found.join(","),caller(0).join("\n")) 83 | # DBlog::sto( tmp ) 84 | # end 85 | # end 86 | 87 | if tstart != nil and tend != nil 88 | where << " ( ? < end and start < ? ) " # 番組表の top と Bottom 89 | args += [ tstart, tend ] 90 | elsif tstart == nil and tend != nil 91 | where << " ? < end " 92 | args << tend 93 | elsif tstart != nil and tend == nil 94 | where << " ? < start " 95 | args << tstart 96 | end 97 | 98 | 99 | if stat != nil 100 | if stat.class == Array 101 | where << " (" + Array.new( stat.size, "stat = ? " ).join(" or ") + ") " 102 | args += stat 103 | else 104 | where << " stat = ? " 105 | args << stat 106 | end 107 | end 108 | 109 | if recpt1pid != nil 110 | where << " recpt1pid = ? " 111 | args << recpt1pid 112 | end 113 | 114 | if skip != nil 115 | where << " skip = ? " 116 | args << skip 117 | end 118 | 119 | if where.size > 0 120 | sql += " where " + where.join(" and ") 121 | end 122 | 123 | if order == nil 124 | sql += " order by id " 125 | else 126 | sql += order 127 | end 128 | if limit != nil 129 | sql += " limit ? " 130 | args << limit 131 | end 132 | sql += ";" 133 | 134 | [ sql, args ] 135 | end 136 | 137 | # 138 | # insert文の生成 139 | # 140 | def makeInsSql( list, para, tname, data ) 141 | args = [] 142 | list.each_pair do |k,v| 143 | args << data[ k ] 144 | end 145 | sql = "insert into #{tname} ( " + para.join(",") + ") values (" 146 | sql += (Array[ "?"] * para.size).join(",") 147 | sql += " )" 148 | [ sql, args ] 149 | end 150 | 151 | # 152 | # update 文の生成 153 | # 154 | def makeUpdSql( list, tname, data, id ) 155 | args = [] 156 | para = [] 157 | list.each_pair do |k,v| 158 | next if k == :id 159 | args << data[ k ] 160 | para << "#{v} = ?" 161 | end 162 | sql = "update #{tname} set " + para.join(",") 163 | sql += " where id = #{id} " 164 | [ sql, args ] 165 | end 166 | 167 | def row2hash( list, row ) 168 | r = [] 169 | if row != nil 170 | row.each do |tmp| 171 | h = {} 172 | list.each_key { |k| h[k] = tmp.shift } 173 | r << h 174 | end 175 | end 176 | r 177 | end 178 | 179 | # 180 | # 最後に insert した rowid を返す。 181 | # 182 | def last_insert_rowid(db) 183 | sql = "select last_insert_rowid();" 184 | r = db.execute( sql ) 185 | r[0][0] 186 | end 187 | 188 | end 189 | -------------------------------------------------------------------------------- /doc/config.md: -------------------------------------------------------------------------------- 1 | 2 | ## config.rb の説明 3 | 4 | * config ファイルは、次の優先度で読み込まれます。 5 | 6 | * 環境変数 RASPIREC_CONF で指定したファイル(拡張子は .rb) 7 | * $HOME/.config/raspirec/config.rb 8 | 9 | 10 | * 配列の定義で、`%w( 1 2 3 )` は `[ "1", "2", "3" ]` と等価です。 11 | 12 | ## 各パラメータの意味は次の通りです。 13 | 14 | #### httpd 15 | 16 | | 定数名 | 説明 | 17 | |--------------------|------| 18 | |Http_port|httpd のポート番号を指定する。
WEBインターフェースには http://XXX.YYY.ZZZ:${Http_port}/ でアクセスする。| 19 | 20 | #### ディレクトリ、ファイル関係 21 | 22 | | 定数名 | 説明 | 23 | |--------------------|------| 24 | |Recpt1_cmd|recpt1 コマンドの path を指定する。| 25 | |Recpt1_opt|recpt1 コマンドのオプションを指定する。b25 デコードをする場合は --b25 を指定する。| 26 | |Epgdump|epgdump コマンドの path を指定する。| 27 | |BaseDir|raspirec がインストールされているディレクトリを設定する。( raspirec.rb があるディレクトリ )| 28 | |DataDir|データベースや録画したファイルの置き場所を指定する。| 29 | 30 | #### 録画タイミング関係 31 | 32 | | 定数名 | 説明 | 33 | |--------------------|------| 34 | |Start_margin|番組開始前の録画マージンを指定(秒)。録画は PC の時計を基準に、開始します。| 35 | |After_margin|番組終了後の録画マージンを指定(秒)。録画は PC の時計を基準に、終了します。| 36 | |Gap_time|録画が連続し、前番組が時短になった場合の、前番組録画終了から次番組開始間隔を指定(秒)。
前番組は Start_margin + Gap_time の秒数だけ録画時間が削られます。
TVチューナー依存ですが、あまり短いと動作が不安定になります。| 37 | 38 | #### チューナー関係 39 | 40 | | 定数名 | 説明 | 41 | |--------------------|------| 42 | |GR_tuner_num|地デジチュナー数| 43 | |BSCS_tuner_num|BSCSチュナー数| 44 | |GBC_tuner_num|地デジ/BS/CS チューナー数| 45 | |Total_tuner_limit|トータルチュナー数制限を掛ける場合に指定する。使用しない場合は false| 46 | 47 | #### EPG関係 48 | 49 | | 定数名 | 説明 | 50 | |--------------------|------| 51 | |GR_EPG_channel|地デジ EPG 受信局を設定する。
東京スカイツリーならば%w( 27 26 25 24 22 23 21 16 ) になります。 参考 | 52 | |BS_EPG_channel|BS EPG 受信局を設定する。BSを受信しない場合は空にする。
通常の番組情報は、どれか 1局だけ受信すれば十分だが、詳細情報を取得するには、その情報を取りたい局を指定する必要がある。| 53 | |CS_EPG_channel|CS EPG 受信局を設定する。CSを受信しない場合は空にする。| 54 | |GR_EpgRsvTime|地デジ EPG受信時間 (秒)| 55 | |BS_EpgRsvTime|BS EPG受信時間 (秒)| 56 | |CS_EpgRsvTime|CS EPG受信時間 (秒)| 57 | |EPGperiod|EPG 取得周期 (H)| 58 | |EpgBanTime|EPG の取得禁止時間帯の指定。(24H制 時間単位) 1時から5時までを禁止したい場合は、[ 1,2,3,4,5 ] と指定する。使用しない場合は nil| 59 | |EPG_tuner_limit|EPG取得時の使用チュナー数に制限を掛ける場合に指定する。使用しない場合は false| 60 | 61 | 62 | #### 自動録画延長機能 63 | | 定数名 | 説明 | 64 | |--------------------|------| 65 | |AutoRecExt | 自動録画延長機能 true で有効。 詳細は こちら を参照 | 66 | |ARE_sampling_time | 番組終了の n 秒前に EPG 採取 | 67 | |ARE_epgdump_opt | 最後尾切り出しの epgdump のオプション | 68 | 69 | 70 | #### ダイアログのオプション初期値 71 | 72 | | 定数名 | 説明 | 73 | |--------------------|------| 74 | |D_FreeOnly|無料放送のみ| 75 | |D_dedupe|重複予約は無効化する| 76 | |D_jitan|チューナー不足の場合に時短を許可| 77 | 78 | #### TSファイル転送 79 | 80 | | 定数名 | 説明 | 81 | |--------------------|------| 82 | |TSFT|true=転送機能有効 true以外=無効
この機能を使うには、送り先のホストに対して,パスワードなしでssh,scpアクセス可能なように設定されていることが必要です。| 83 | |TSFT_host|送り先 ホスト名| 84 | |TSFT_user|送り先 login名| 85 | |TSFT_toDir|送り先Dir| 86 | |TSFT_rate|想定転送速度 ( Mbyte/秒 )
この数字を使って、空き時間に転送する/しないを判断します。| 87 | 88 | #### HLS モニタ機能 89 | 90 | | 定数名 | 説明 | 91 | |--------------------|------| 92 | |MonitorFunc|true=モニタ機能を有効, false=無効。 無効の場合は、下記のパラメータは無視される。設定方法は こちら を参照して下さい。| 93 | |StreamDir|ストリーム出力先ディレクトリ| 94 | |MonitorWidth|モニタ画面の横幅| 95 | |HlsConvCmd|HLS変換コマンド| 96 | 97 | #### mpv モニタ機能 98 | 99 | | 定数名 | 説明 | 100 | |--------------------|------| 101 | |MPMonitor|true = mpv モニタ機能を有効、false = 無効
無効の場合は、下記のパラメータは無視される。設定方法は こちら を参照して下さい。| 102 | |DevAutoDetection| DeviceList_GR, DeviceList_BSCS, DeviceList_GBC を自動設定する。true = 有効。| 103 | |DeviceList_GR|地デジのデバイスファイルを指定する。| 104 | |DeviceList_BSCS|BS,CS のデバイスファイルを指定する。| 105 | |DeviceList_GBC | 地デジ,BS,CS のデバイスファイルを指定する。| 106 | |Mpv_cmd|mpv の実行ファイル名(絶対パス)| 107 | |Mpv_opt|mpv の引数を指定する。 108 | |RemoteMonitor|表示するマシンが別の場合 ture, 同一の場合に false| 109 | |UDPbasePort|使用する UDP のポート番号、デバイスの数だけプラスされる。| 110 | |XServerName|RemoteMonitor が true の場合に、mpvを実行、表示するマシンのマシン名を設定する。| 111 | |RecHostName|RemoteMonitor が true の場合に、チューナー(raspirecが実行されている)のマシン名を設定する。| 112 | |Lsof_cmd|lsof コマンドへのパス。Ubuntu では /usr/bin/lsof | 113 | |Browser_cmd| 番組表を表示するブラウザの実行ファイル名(絶対パス) | 114 | |RaspirecTV_font|フォント指定| 115 | |RaspirecTV_GEO|座標指定 ( WxH+X+Y or X+Y )| 116 | |RaspirecTV_SOCAT|socat の実行ファイル名(絶対パス)(RemoteMonitor == true の時のみ)| 117 | 118 | #### その他 119 | 120 | | 定数名 | 説明 | 121 | |--------------------|------| 122 | |TSnameFormat|TSファイル名の生成ルール。詳細は 「コントロールパネル/補足説明」を参照して下さい。| 123 | |LogSaveDay|ログの保持期間(日)| 124 | |RsvHisSaveDay|録画済み記録の保持期間(日)| 125 | |DiskKeepPercent|録画したTSファイルを古い順に削除して指定したDisk容量(%)を確保する。(指定するのは空き容量)
機能を無効にする場合は false を指定する。| 126 | |Local_jquery|オフライン環境で動作させる為に jquery, materialize のライブラリをローカルにコピーした場合に、true にする。
詳細は doc/jquery_local.md を参照の事。 通常は false| 127 | |StationPage|番組表で、1ページ当たりの放送局数 (個) の初期値| 128 | |Debug|ture で Debug モード。ログファイルを出力するようになる。オプションで、 -d を指定するのと同じ。| 129 | |Debug_mem|ture で メモリの消費量をモニタするようになる。| 130 | |TitleRegex|「自動予約」ボタンを押して番組検索に遷移した時に、番組タイトルから「題名」を生成する為の、余計な文字を削除する正規表現の配列| 131 | |SearchStringRegex|「自動予約」ボタンを押して番組検索に遷移した時に、番組タイトルから「検索文字列」を生成する為の、余計な文字を削除する正規表現の配列| 132 | |EpgPatchEnable|EPGPatch機能を有効にする。 false = 無効。デフォルトは有効| -------------------------------------------------------------------------------- /raspirec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby 2 | # coding: utf-8 3 | # 4 | 5 | 6 | # 7 | # main 8 | # 9 | require 'rubygems' 10 | require 'optparse' 11 | require 'sqlite3' 12 | 13 | base = File.dirname( $0 ) 14 | $: << base + "/src" 15 | 16 | require 'require.rb' 17 | 18 | 19 | $opt = { 20 | :f => false, # forground 21 | :lc => false, # log clear 22 | :dc => false, # db clear 23 | :conf => nil, # config file 指定 24 | } 25 | $debug = Debug 26 | LoopMax = 99 27 | 28 | OptionParser.new do |opt| 29 | opt.on('-d') {|v| $debug = true } 30 | opt.on('-f') {|v| $opt[:f] = !$opt[:f] } 31 | opt.on('--kill') { Control.new.stop(); exit } # kill process 32 | opt.on('--lc') {|v| $opt[:lc] = true } # log clear 33 | opt.on('--dc') {|v| $opt[:dc] = true } # db clear 34 | opt.on('--config FILE') {|v| $opt[:conf] = v } # config 35 | opt.parse!(ARGV) 36 | end 37 | 38 | # 39 | # 終了処理 40 | # 41 | def endParoc() 42 | $endParoc = true 43 | count = 0 44 | [ $timer_pid, $httpd_pid ].each do |pid| 45 | begin 46 | DBlog::sto( "main::endParoc() kill #{pid}" ) 47 | Process.kill(:TERM, pid ); 48 | count += 1 49 | rescue 50 | end 51 | end 52 | count.times do |n| 53 | sleep(1) 54 | putc(".") 55 | STDOUT.flush 56 | end 57 | exit 58 | end 59 | 60 | 61 | # 62 | # デーモン化 63 | # 64 | def daemonStart( ) 65 | if fork # 親 66 | exit!(0) 67 | else # 子 68 | Process::setsid 69 | if fork # 親 70 | exit!(0) 71 | else # 子 72 | Dir::chdir("/") 73 | File::umask(0) 74 | 75 | STDIN.reopen("/dev/null") 76 | if $debug == true 77 | STDOUT.reopen( StdoutM, "w") 78 | STDERR.reopen( StderrM, "w") 79 | else 80 | STDOUT.reopen("/dev/null", "w") 81 | STDERR.reopen("/dev/null", "w") 82 | end 83 | yield if block_given? 84 | end 85 | end 86 | end 87 | 88 | $timer_main = "#{SrcDir}/timer_main.rb" 89 | $httpd_main = "#{SrcDir}/httpd_main.rb" 90 | 91 | # 92 | # main loop 93 | # 94 | def mainLoop() 95 | DBlog::sto("main loop start #{Commlib::getVer()}") 96 | 97 | File.open( PidFile, "w") do |fp| 98 | fp.puts( Process.pid ) 99 | end 100 | 101 | if $opt[:conf] != nil 102 | ENV["RASPIREC_CONF_OPT"] = $opt[:conf] 103 | end 104 | 105 | if Object.const_defined?(:DevAutoDetection) == true and 106 | DevAutoDetection == true 107 | DeviceChk.new.run 108 | end 109 | 110 | pids = [] 111 | pids << Thread.new do 112 | tcount = 0 113 | while true 114 | $timer_pid = fork do 115 | args = [ $timer_main ] 116 | args << "--debug" if $debug == true 117 | exec("ruby", *args ) 118 | end 119 | Process.waitpid( $timer_pid ) 120 | DBlog::warn(nil, "timer_main end") 121 | sleep 5 122 | break if $endParoc == true 123 | break if tcount > LoopMax 124 | tcount += 1 125 | end 126 | end 127 | 128 | pids << Thread.new do 129 | hcount = 0 130 | while true 131 | $httpd_pid = fork do 132 | args = [ $httpd_main, "-o", "0.0.0.0"] 133 | if Http_port != nil and Http_port > 0 134 | args += [ "-p", Http_port.to_s ] 135 | end 136 | args += %w( -- --debug ) if $debug == true 137 | exec("ruby", *args ) 138 | end 139 | Process.waitpid( $httpd_pid ) 140 | DBlog::warn(nil, "httpd_main end") 141 | sleep 5 142 | break if $endParoc == true 143 | break if hcount > LoopMax 144 | hcount += 1 145 | end 146 | end 147 | 148 | if $opt[:tail] == true 149 | pids << Thread.new do 150 | tailLog() 151 | end 152 | end 153 | 154 | pids.each {|t| t.join} 155 | 156 | end 157 | 158 | 159 | # 160 | # 初期化 161 | # 162 | rmlist = [] 163 | if $opt[:lc] == true 164 | rmlist += [ 165 | EPGLockFN, 166 | DbupdateFN, 167 | LogFname, 168 | StdoutM, 169 | StderrM, 170 | StdoutH, 171 | StderrH, 172 | StdoutT, 173 | StderrT 174 | ] 175 | end 176 | 177 | if $opt[:dc] == true 178 | rmlist << DbFname 179 | end 180 | 181 | if rmlist.size > 0 182 | rmlist.each do |fn| 183 | if test( ?f, fn ) 184 | pp "unlink(#{fn})" 185 | File.unlink( fn ) 186 | File.open( fn, "w" ) {|fp| } unless fn == DbFname 187 | end 188 | end 189 | exit 190 | end 191 | 192 | 193 | 194 | 195 | # 196 | # 初期化 197 | # 198 | Commlib::makeSubDir() 199 | 200 | File.open( MainLockFN, File::RDWR|File::CREAT, 0644) do |fl| 201 | if fl.flock(File::LOCK_EX|File::LOCK_NB) == false 202 | puts("raspirec locked\n") 203 | exit 204 | end 205 | 206 | [ EPGLockFN ].each do |fn| 207 | if test(?f, fn ) 208 | File.unlink( fn ) 209 | end 210 | end 211 | $rec_pid = {} 212 | $endParoc = false 213 | 214 | setTrap() 215 | 216 | [ $timer_main , $httpd_main].each do |fname| 217 | unless test( ?f, fname ) 218 | puts("Error: #{fname} not found") 219 | exit 220 | end 221 | end 222 | 223 | if $opt[:f] == false 224 | daemonStart{ mainLoop() } 225 | else 226 | mainLoop() 227 | end 228 | end 229 | 230 | 231 | 232 | --------------------------------------------------------------------------------