From: satomichan Date: Thu, 25 Sep 2025 17:04:14 +0000 (+0900) Subject: NHK番組表取得ツール get-nhk-title.pl 新規実装, 日付を省略できるように. X-Git-Url: https://satomichan.jp/gitweb/?a=commitdiff_plain;h=f466172f2b4bd71d2032dcbb535c83a4ab8afd79;p=rec-radiko.git NHK番組表取得ツール get-nhk-title.pl 新規実装, 日付を省略できるように. 日付が省略されたとき, こんどの (現在時刻以降の直近の) 指定時刻の日付 (今日または明日) を指定したとみなす. --- diff --git a/get-nhk-title.pl b/get-nhk-title.pl new file mode 100755 index 0000000..578cf2f --- /dev/null +++ b/get-nhk-title.pl @@ -0,0 +1,282 @@ +#!/usr/bin/env -S perl -w + + sub usage {print Encode::decode('UTF-8',<<'_END_OF_USAGE_'); +get-nhk-title.pl ---- NHK番組表 取得ツール + (https://satomichan.jp/rec-radiko) + +NHKオンライン テキスト版 (https://k.nhk.jp/) で提供されている +番組表から 番組情報(放送長さ・タイトル・プレイリスト等の詳細情報) を +取得するツールです. + +使い方: + get-nhk-title.pl [--short | -s] [--escape | -e] + [--li ] [--nominutes | -m] + + + には放送局を指定します. 指定できるのは, + r2 (第2放送), + sapporo-r1, sendai-r1, tokyo-r1, nagoya-r1, osaka-r1, + hiroshima-r1, matsuyama-r1, fukuoka-r1 (ここまで第1放送), + sapporo-fm, sendai-fm, tokyo-fm, nagoya-fm, osaka-fm, + hiroshima-fm, matsuyama-fm, fukuoka-fm (ここまでFM放送) です. + + には番組開始の日付(暦日)を指定します. + 書式は YYYY-MM-DD です. 例) 2025-09-01 (2025å¹´9月1日の場合) + + には番組開始の時刻を指定します. + 書式は [h]h:mm または [h]hmm です (24時間制). + 深夜24時以降を指定する場合は, 日付を翌日にし, + 0:00 以降の時刻を指定します. + コロンはあってもなくても構いません. 時の先頭の 0 は省略可能です. + 指定の日時にちょうど始まる番組の情報が取得されます. + 例) 12:30 (昼の12時半の場合), 905 (午前9時05分の場合), + 1515 (午後3時15分の場合), 0305 (深夜3時05分の場合) + + --short または -s を指定しないと Long出力モードになります. + 放送長さ・番組タイトル・プレイリスト等の詳細情報が複数行にわたって + 出力されます. + + --short または -s を指定すると Short出力モードになります. + 放送長さ・番組タイトルが 1つの半角スペースで区切って出力されます. + + Short出力モード のときに --escape または -e も指定されていると, + 番組タイトルのうち スペース類を _ に, 半角の記号類を全角に変換して + 出力します. + + Short出力モード のときに --li も指定されていると, + 番組タイトルに使われる 番組表内部 HTML の li要素 が + 番目のものに変更されます. + デフォルトでは 1 行目となっています(--li 1 と等価). + には 1 以上の自然数を指定します. カンマ(,)で区切って + 複数指定することも出来ます. 例) --li 3,4 + + Short出力モード のときに --shinyabin を指定すると, + 番組が ラジオ深夜便 だった場合に 番組タイトルを 3,4行目などを組み + 合わせたものを出力します (1行目のみだと特集内容がわからないため). + + + --nominutes または -m を指定されていると, 放送長さ を出力しません. + (Long出力モード, Short出力モード ともに) + +実行例: + get-nhk-title.pl -s -e --shinyabin tokyo-r1 2025-09-08 1230 + +留意事項: + 当然ですが, 現行法規で認められた範囲内かつ NHK が認める範囲内で + ご使用ください. +_END_OF_USAGE_ +} + +# Copyright 2025 FUKUDA Satomi (https://satomichan.jp/) +# +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# +# See the License for the specific language governing permissions and +# limitations under the License. + +use strict; +use warnings; + +use utf8; +binmode STDOUT, ":utf8"; +binmode STDERR, ":utf8"; + +use HTTP::Tiny; #sudo apt install libhttp-tiny-perl +use Encode; +use HTML::Entities; +use Getopt::Long qw(:config posix_default no_ignore_case gnu_compat); + + + +#オプション解析 +my %option = (li => 1); +GetOptions( \%option, ('short|s', 'li=s', 'nominutes|m', 'escape|e', 'shinyabin') ); + +my ($station, $date, $time) = @ARGV; + +usage(), exit unless ($station && $date && $time); + +my ($day) = $date =~ /^\d{4}-\d{2}-(\d{2})$/ or die "Date Format Error ('$date' must be YYYY-MM-DD)"; +my ($h, $m) = $time =~ /^(\d{1,2}):?(\d{2})$/ or die "Time Format Error ('$time' must be HHMM or HH:MM)"; + +my ($area, $ch) = @{station2area_channel($station)}; + + +#午前0時-4時台のときは前日の番組表を参照する +if ($h < 5) { + use Time::Piece; + my $t = Time::Piece->strptime($date, '%Y-%m-%d'); + $t -= Time::Seconds::ONE_DAY; + $date = $t->ymd; +} + +#番組情報(長さ・タイトル・詳細)出力 +my $program = get_timetable($area, $ch, $date, $day, $h, $m); +if ($program) { + if ($option{short}) { + #Short 表示モード + print "$program->{minutes} " unless $option{nominutes}; + + my $title = ''; + + #ラジオ深夜便対策 + if ($option{shinyabin} && $program->{list}[0] =~ /^ラジオ深夜便/) { + $title = 'ラジオ深夜便'; + $option{li} = '3,4'; + } + + #タイトル取得・エスケープ + $title .= $program->{list}[$_ -1] for split(/,/, $option{li}); + $title = escape($title) if $option{escape}; + + print "$title"; + + }else{ + #Long 表示モード + print $option{nominutes} ? "" : "$program->{minutes}分間\n"; + print "$_\n" for @{$program->{list}}; + } +} + +exit; + + + +#番組表 GET +sub get_timetable { + my ($area, $ch, $tt_ymd, $d, $h, $m) = @_; + # └番組表の年月日, └暦日 + + my $kanji_time = kanji_time($d, $h, $m); + my $url = "https://k.nhk.jp/timetable/read/c.html?a=$area&c=$ch&d=$tt_ymd&f=top"; + + my $resp = HTTP::Tiny->new->get($url); + + if ($resp->{success}) { + my $body = Encode::decode('UTF-8', $resp->{content}); + + my ($minutes, $entry) = + $body =~ m|
${kanji_time}から.+?分(放送時間(\d{1,3})分間)
    (.+?)
| + or die "Cannot get program info starting at the specified date-time. TIME: ${kanji_time} URL: $url"; + + $entry = HTML::Entities::decode_entities($entry); #HTML文字実体参照(&xxxx;) デコード + $entry =~ s/
/\n/g; + + my @list = $entry=~ m|
  • (.+?)
  • |sg; + return {minutes => $minutes, list => \@list}; + + }else{ + die "Not success GET : $url"; + } +} + + + +#「日・時・分」から 「(0日)?(午前|午後)0時00分」文字列を生成 +sub kanji_time { + my ($d, $h, $m) = @_; + + my $day = ''; + my $ampm; + + if ($h < 5) { + $day = sprintf('%i日', $d); + $ampm = '午前'; + + }elsif ($h <= 11) { + $ampm = '午前'; + + }else{ + $ampm = '午後'; + $h -= 12; + } + + return sprintf("%s%s%i時%02i分", $day, $ampm, $h, $m); +} + + + +#エリアコード・チャンネルコード +sub station2area_channel { + my ($station) = @_; + + return ['001','06'] if $station eq 'r2'; + + my ($base, $channel) = $station =~ /^(.\w+)-(r1|fm)$/ or die "'$station' : Unknown station."; + + my %area_code = ( + sapporo => '700', + sendai => '600', + tokyo => '001', + nagoya => '300', + osaka => '200', + hiroshima => '400', + matsuyama => '800', + fukuoka => '501', + ); + + die "'$station' : Unknown area." unless $area_code{$base}; + + + my %channel_code = ( + r1 => '05', + fm => '07', + ); + + die "'$station' : Unknown area." unless $channel_code{$channel}; + + + return [$area_code{$base}, $channel_code{$channel}]; +} + + + +#ファイル名不適記号類を全角文字化 +sub escape { + my($str) = @_; + + #空白 -> _ にする + $str =~ s/\s+/_/g; + $str =~ s/ +/_/g; + + #半角記号類 -> 全角にする + for ($str) { + s/"/”/g; + s/'/’/g; + s/`/`/g; + s/,/,/g; + s/\././g; + s//>/g; + s/\|/|/g; + s/:/:/g; + s/;/;/g; + s/\?/?/g; + s/!/!/g; + s/&/&/g; + s/%/%/g; + s/~/ï¿£/g; + s/\$/$/g; + s/\*/*/g; + s/\\/ï¿¥/g; + s/\////g; + s/\././g; + s/\+/+/g; + s/\(/(/g; + s/\)/)/g; + s/\[/ï¼»/g; + s/]/ï¼½/g; + s/{/{/g; + s/}/}/g; + } + + return $str; +} + diff --git a/rec-nhk-radio.pl b/rec-nhk-radio.pl old mode 100644 new mode 100755 index 59523d7..2dc04de --- a/rec-nhk-radio.pl +++ b/rec-nhk-radio.pl @@ -32,6 +32,8 @@ binmode STDERR, ":utf8"; use File::Basename; use Getopt::Long qw(:config posix_default no_ignore_case gnu_compat); +use Time::Piece; +use Encode; @@ -44,6 +46,7 @@ my $ECHO = `which echo | tr -d '\n'`; my $SLEEP = `which sleep | tr -d '\n'`; my $RM = `which rm | tr -d '\n'`; my $CHMOD = `which chmod | tr -d '\n'`; +my $GETTT = `which get-nhk-title.pl | tr -d '\n'`; @@ -57,6 +60,7 @@ die "保存先 $DIR_SAVE_TO に書き込み権限がありません." # 動作モード my $is_check_mode = 0; +my $is_verbose_mode = 0; my $is_conv_to_mp3 = 1; my $SLEEP_TIME_SEC = 5; my $FILE_PERMISSION = 644; @@ -65,9 +69,10 @@ my $FILE_PERMISSION = 644; # オプション解析 my %opts; -GetOptions( \%opts, ("check|c", "no-conversion-to-mp3|n") ); -$is_check_mode = 1 if $opts{'check'}; -$is_conv_to_mp3 = 0 if $opts{'no-conversion-to-mp3'}; +GetOptions( \%opts, ("check|c", "verbose", "no-conversion-to-mp3|n") ); +$is_check_mode = 1 if $opts{'check'}; +$is_verbose_mode = 1 if $opts{'verbose'}; +$is_conv_to_mp3 = 0 if $opts{'no-conversion-to-mp3'}; @@ -75,11 +80,15 @@ $is_conv_to_mp3 = 0 if $opts{'no-conversion-to-mp3'}; unless (@ARGV) { my $script_name = basename($0, ''); my $usage = << " EOM_USAGE"; - USAGE) $script_name [-c|--check] [-n|--no-conversion-to-mp3] <放送局ID> <録音開始日> <録音開始時刻> <録音長(分)> <タイトル> + USAGE) $script_name [-c|--check] [--verbose] [-n|--no-conversion-to-mp3] + <放送局ID> <録音開始日> <録音開始時刻> [<録音長(分)> <タイトル>] ex) $script_name tokyo-fm 2025-05-31 1230 91 scramble_taniyama-hiroko ex) $script_name --check tokyo-fm 2025-05-31 12:30 91 scramble_taniyama-hiroko + ex) $script_name sendai-fm 2025-09-01 1230 <放送局ID>は radi.sh -l で確認できます. ex) tokyo-r1, r2, tokyo-fm + <録音長(分)> <タイトル> を省略したときには, get-nhk-title.pl を使用して取得します. + <録音開始日> を省略したときには, 現在時刻以降の直近の <録音開始時刻> から録音を開始します. EOM_USAGE @@ -90,15 +99,36 @@ unless (@ARGV) { # 引数 解析 -my($station, $date, $time, $min, $title) = @ARGV; +my($station) = shift; -my($y, $m, $d) = split(/-/, $date); +my($date, $time) = (shift, shift); + +#日付が無くて, その場所に時刻があったとき +# -> 現在時刻以降の直近のその時刻の日になるように, 日付を今日or明日 から選択して設定 +if ($date !~ /^\d{4}-\d{2}-\d{2}$/ && $date =~ /\d{1,2}:?\d{2}/) { + unshift @ARGV, $time; + $time = $date; + $date = get_date_next_time($time); +} + +my($y, $m, $d) = split(/-/, $date); my($time_h, $time_m) = ($1, $2) if $time =~ /(\d{1,2}):?(\d{2})/; + +my($min, $title) = @ARGV; + +#番組長さ・タイトルを 番組表から取得 +if (!$min && !$title && $GETTT) { + my $minutes_title = `$GETTT --short --escape --shinyabin $station $date $time`; + ($min, $title) = split(/ /, $minutes_title); + $min++; +} + +$title = Encode::decode('UTF-8', $title); $title =~ s/\s+/_/g; die "無効な引数です." unless ($y && $m && $d && $time_h <= 24 && $time_h >= 0 && - $time_m <= 59 && $time_m >= 0 && $min > 0 && length $title); + $time_m <= 59 && $time_m >= 0 && $min > 0 && length $title); -my $file_name_base = sprintf('%04d-%02d-%02d_%02d%02d_nhk-%s_%s', $y, $m, $d, $time_h, $time_m, $station, $title); +my $file_name_base = sprintf('%04d-%02d-%02d_%02d%02d_nhk-%s_%s_%imin', $y, $m, $d, $time_h, $time_m, $station, $title, $min); my $m4a = "${file_name_base}.m4a"; my $mp3 = "${file_name_base}.mp3"; @@ -121,9 +151,37 @@ my $cmd = qq{$ECHO -e '$BASH << EOC\\n$rec_cmd\\nEOC' | $AT "$at_time"}; if ($is_check_mode) { print "$cmd\n\n"; exit; + +} elsif($is_verbose_mode) { + print "$cmd\n\n"; + system("$GETTT $station $date $time"); + + chdir $DIR_SAVE_TO; + system($cmd); + exit; + } else { chdir $DIR_SAVE_TO; system($cmd); exit; } + + +#こんどの (現在時刻以降の直近の) 指定時刻 のときの日付(今日or明日) +sub get_date_next_time { + my ($hhmm) = @_; + + my ($h, $m) = $hhmm =~ /^(\d{1,2}):?(\d{2})$/ + or die "Time Format Error ('$time' must be HHMM or HH:MM)"; + + my $now = localtime; + #現在時刻 < 指定時刻 ? + if (($now->hour * 100 + $now->min) < ($h * 100 + $m)) { + #今日の日付 + return $now->ymd; + }else{ + #明日の日付 + return ($now + Time::Seconds::ONE_DAY)->ymd; + } +}