1 #!/usr/bin/env -S perl -w
3 # foltia-dl.pl ---- foltia ANIME LOCKER 録画データ ダウンローダ
4 # (https://satomichan.jp/foltia-dl)
6 # foltia ANIME LOCKER (https://foltia.com/ANILOC/) が動作している
7 # Webサイトから録画・録音ファイルをダウンロードし,
8 # メタデータ(番組名・放送局名・放送時間)を付加して保存するツールです.
11 # foltia-dl.pl [--tsv] [--grep <title_regexp_string>]
13 # --tsv または -t を指定すると, タブ区切りテキスト出力モードになります.
14 # 指定しないときが通常モードで, シェルで実行可能な文字列を出力します.
15 # --grep <正規表現文字列> を設定すると, 番組名が<正規表現文字列>に
19 # foltia-dl.pl --grep 'きょうの(料理|健康)' | bash
21 # $FOLTIA_HOST を指定している箇所は, 環境に合わせて foltia ANIME LOCKER が
22 # 動作しているホスト名またはIPアドレスに書き換えてください.
25 # Copyright 2025 FUKUDA Satomi (https://satomichan.jp/)
27 # Licensed under the Apache License, Version 2.0 (the “License”);
28 # you may not use this file except in compliance with the License.
29 # You may obtain a copy of the License at
30 # http://www.apache.org/licenses/LICENSE-2.0
32 # Unless required by applicable law or agreed to in writing, software
33 # distributed under the License is distributed on an “AS IS” BASIS,
34 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
36 # See the License for the specific language governing permissions and
37 # limitations under the License.
43 binmode STDOUT, ":utf8";
44 binmode STDERR, ":utf8";
48 use HTML::Entities; # apt install libhtml-parser-perl
49 use Getopt::Long qw(:config posix_default no_ignore_case gnu_compat);
51 #要変更箇所 お使いの環境に合わせて書き換えてください.
52 # (foltia ANIME LOCKER が動作しているホスト名またはIPアドレス)
53 our $FOLTIA_HOST = 'http://FOLTIA-ANIME-LOCKER.test';
55 #保存ファイル名(拡張子を除いた部分)の最大長さ(bytes)
56 our $LIMIT_LENGTH_OF_FILE_BASE_NAME = 80;
62 GetOptions( \%opts, ('tsv|t', 'grep=s') );
63 our ($is_tsv_mode, $is_cmdline_mode, $opts_grep);
65 #TSVモード or コマンドラインモード(通常)
73 $opts_grep = Encode::decode('UTF-8', $opts{'grep'});
80 get_rec_table('/recorded/recfiles_all.php');
82 print STDERR "ダウンロード対象 番組数:$cnt_programs ファイル数:@{[ 0+ keys %used_filename ]}\n";
90 my $resp = HTTP::Tiny->new->get($FOLTIA_HOST. $a_path);
92 if ($resp->{success}) {
93 my $body = Encode::decode('UTF-8', $resp->{content});
96 while ($body =~ m|<tr.+?</tr>|sg) {
98 $rec =~ s/(<br>)|\n|\r//g;
100 my ($chapter) = $rec =~ m|<td class="chapter">(.+?)</td>|; #話数
102 next unless $chapter; #話数がなければ番組ではない
103 next if $rec =~ /watchnow/; #録画中
105 my ($pid) = $rec =~ m|pid=(-\d+)|;
108 if ( $rec =~ m|<td class="date">(\d{4})/(\d{2})/(\d{2})\(.+\) (\d{2}):(\d{2})</td>| ) {
109 $date = "$1-$2-$3T$4:$5";
112 my ($title) = $rec =~ m|<td class="subtitle">(.+?)</td>|;
113 $title = HTML::Entities::decode_entities($title); #HTML文字実体参照(&xxxx;) デコード
115 my (@paths) = $rec =~ m|<a href='(/tv/[-/\w\.]+?)'>|g;
117 die "日時・番組名が取得できませんでした. rec=$rec" unless $date && $title;
121 next unless $title =~ /$opts_grep/;
128 my($station, $min) = ('', '');
129 if ($pid) { #テレビ番組のとき
130 ($station, $min) = get_detail($pid);
133 ($station) = $paths[0] =~ m|-(\w+)\.\w+$|;
136 #print "($chapter,@{[ $pid ? $pid : '' ]}) $date, $title, $station, $min, {@paths}\n";
139 my $basename = make_basename($date, $title);
143 foreach my $a_path (@paths) {
146 my ($ext) = $a_path =~ /\.(\w+)$/;
150 $basename .= "_c$chapter" if $used_filename{"$basename.$ext"};
152 $used_filename{"$basename.$ext"} = 1;
156 my @outputs = ("$basename.$ext", $a_path, $title, $station, "$date (${min}min)");
157 print join("\t", @outputs). "\n";
159 } elsif ($is_cmdline_mode) {
161 print "wget --continue -O '$basename.$ext' '$FOLTIA_HOST$a_path'";
163 #.mp4のとき -> メタ情報を付加するコマンドも出力
165 $title =~ s/'/'\\''/g;
166 print " && ffmpeg -nostdin -i '$basename.$ext' -metadata title='$title' -metadata artist='$station' ".
167 "-metadata date='$date' -metadata comment='$date (${min}min)' -codec copy $chapter.$ext";
168 print " && mv $chapter.$ext '$basename.$ext'";
181 get_rec_table($1) if $body =~ m|<a rel=next href="(.+?)" >|;
184 die "録画一覧が取得できませんでした. get=$FOLTIA_HOST$a_path";
194 my $detail_url = "${FOLTIA_HOST}/recorded/selectcaptureimage.php?pid=$pid";
196 my $resp = HTTP::Tiny->new->get($detail_url);
198 if ($resp->{success}) {
199 my $body = Encode::decode('UTF-8', $resp->{content});
201 #my ($title) = $body =~ m|<em>サブタイトル</em><span>:</span><strong>(.+?)</strong>|;
202 my ($station) = $body =~ m|<em>放送局</em><span>:</span><strong>(.+?)</strong>|;
203 my ($min) = $body =~ m|<em>放送時間/分</em><span>:</span><strong>(\d+?)</strong>|;
205 die "詳細情報(放送局・放送時間)が取得できませんでした. detail_url=$detail_url" unless $station && $min;
207 return ($station, $min);
218 my($date, $title) = @_;
220 $date =~ s/T\d{2}:\d{2}$//; #時刻部を除去
222 my $basename = "${date}_${title}";
225 $basename =~ s/\s+/_/g;
226 $basename =~ s/ +/_/g;
259 $basename = substr($basename, 0, $LIMIT_LENGTH_OF_FILE_BASE_NAME);