--- /dev/null
+#!/usr/bin/env -S perl -w
+
+# foltia-dl.pl ---- foltia ANIME LOCKER 録画データ ダウンローダ
+# (https://satomichan.jp/foltia-dl)
+#
+# foltia ANIME LOCKER (https://foltia.com/ANILOC/) が動作している
+# Webサイトから録画・録音ファイルをダウンロードし,
+# メタデータ(番組名・放送局名・放送時間)を付加して保存するツールです.
+#
+# 使い方:
+# foltia-dl.pl [--tsv] [--grep <title_regexp_string>]
+#
+# --tsv または -t を指定すると, タブ区切りテキスト出力モードになります.
+# 指定しないときが通常モードで, シェルで実行可能な文字列を出力します.
+# --grep <正規表現文字列> を設定すると, 番組名が<正規表現文字列>に
+# 合致した場合のみ出力します.
+#
+# 実行例:
+# foltia-dl.pl --grep 'きょうの(料理|健康)' | bash
+#
+# $FOLTIA_HOST を指定している箇所は, 環境に合わせて foltia ANIME LOCKER が
+# 動作しているホスト名またはIPアドレスに書き換えてください.
+#
+#
+# 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;
+use Encode;
+use HTML::Entities; # apt install libhtml-parser-perl
+use Getopt::Long qw(:config posix_default no_ignore_case gnu_compat);
+
+#要変更箇所 お使いの環境に合わせて書き換えてください.
+# (foltia ANIME LOCKER が動作しているホスト名またはIPアドレス)
+our $FOLTIA_HOST = 'http://FOLTIA-ANIME-LOCKER.test';
+
+#保存ファイル名(拡張子を除いた部分)の最大長さ(bytes)
+our $LIMIT_LENGTH_OF_FILE_BASE_NAME = 80;
+
+
+
+#オプション解析
+my %opts;
+GetOptions( \%opts, ('tsv|t', 'grep=s') );
+our ($is_tsv_mode, $is_cmdline_mode, $opts_grep);
+
+#TSVモード or コマンドラインモード(通常)
+if ($opts{'tsv'}) {
+ $is_tsv_mode = 1;
+}else{
+ $is_cmdline_mode = 1;
+}
+
+#番組名grep
+$opts_grep = Encode::decode('UTF-8', $opts{'grep'});
+
+
+
+my $cnt_programs = 0;
+my %used_filename;
+
+get_rec_table('/recorded/recfiles_all.php');
+
+print STDERR "ダウンロード対象 番組数:$cnt_programs ファイル数:@{[ 0+ keys %used_filename ]}\n";
+
+exit;
+
+
+
+sub get_rec_table {
+ my ($a_path) = @_;
+ my $resp = HTTP::Tiny->new->get($FOLTIA_HOST. $a_path);
+
+ if ($resp->{success}) {
+ my $body = Encode::decode('UTF-8', $resp->{content});
+
+ #レコード(番組)ごとのループ
+ while ($body =~ m|<tr.+?</tr>|sg) {
+ my $rec = $&;
+ $rec =~ s/(<br>)|\n|\r//g;
+
+ my ($chapter) = $rec =~ m|<td class="chapter">(.+?)</td>|; #話数
+
+ next unless $chapter; #話数がなければ番組ではない
+ next if $rec =~ /watchnow/; #録画中
+
+ my ($pid) = $rec =~ m|pid=(-\d+)|;
+
+ my $date;
+ if ( $rec =~ m|<td class="date">(\d{4})/(\d{2})/(\d{2})\(.+\) (\d{2}):(\d{2})</td>| ) {
+ $date = "$1-$2-$3T$4:$5";
+ }
+
+ my ($title) = $rec =~ m|<td class="subtitle">(.+?)</td>|;
+ $title = HTML::Entities::decode_entities($title); #HTML文字実体参照(&xxxx;) デコード
+
+ my (@paths) = $rec =~ m|<a href='(/tv/[-/\w\.]+?)'>|g;
+
+ die "日時・番組名が取得できませんでした. rec=$rec" unless $date && $title;
+
+ #番組名grep
+ if ($opts_grep) {
+ next unless $title =~ /$opts_grep/;
+ }
+
+ #ここまできたら, DL対象の番組である
+ $cnt_programs++;
+
+ #放送局・放送長さ取得
+ my($station, $min) = ('', '');
+ if ($pid) { #テレビ番組のとき
+ ($station, $min) = get_detail($pid);
+
+ }else{ #ラジオ番組のとき
+ ($station) = $paths[0] =~ m|-(\w+)\.\w+$|;
+ }
+
+ #print "($chapter,@{[ $pid ? $pid : '' ]}) $date, $title, $station, $min, {@paths}\n";
+
+ #ファイル名 基本部
+ my $basename = make_basename($date, $title);
+
+
+ #DL対象ファイルごとのループ
+ foreach my $a_path (@paths) {
+
+ #拡張子
+ my ($ext) = $a_path =~ /\.(\w+)$/;
+ $ext = lc $ext;
+
+ #ファイル名 重複あるとき
+ $basename .= "_c$chapter" if $used_filename{"$basename.$ext"};
+
+ $used_filename{"$basename.$ext"} = 1;
+
+ if ($is_tsv_mode) {
+ #TSV出力モード
+ my @outputs = ("$basename.$ext", $a_path, $title, $station, "$date (${min}min)");
+ print join("\t", @outputs). "\n";
+
+ } elsif ($is_cmdline_mode) {
+ #コマンドライン出力モード
+ print "wget --continue -O '$basename.$ext' '$FOLTIA_HOST$a_path'";
+
+ #.mp4のとき -> メタ情報を付加するコマンドも出力
+ if ($ext eq 'mp4') {
+ $title =~ s/'/'\\''/g;
+ print " && ffmpeg -nostdin -i '$basename.$ext' -metadata title='$title' -metadata artist='$station' ".
+ "-metadata date='$date' -metadata comment='$date (${min}min)' -codec copy $chapter.$ext";
+ print " && mv $chapter.$ext '$basename.$ext'";
+ }
+
+ print "\n";
+ }
+
+
+ } #DL対象ファイルごとのループ
+
+
+ } #レコード(番組)ごとのループ
+
+ #次の HTML ページ
+ get_rec_table($1) if $body =~ m|<a rel=next href="(.+?)" >|;
+
+ } else {
+ die "録画一覧が取得できませんでした. get=$FOLTIA_HOST$a_path";
+ }
+
+}
+
+
+
+sub get_detail {
+ my ($pid) = @_;
+
+ my $detail_url = "${FOLTIA_HOST}/recorded/selectcaptureimage.php?pid=$pid";
+
+ my $resp = HTTP::Tiny->new->get($detail_url);
+
+ if ($resp->{success}) {
+ my $body = Encode::decode('UTF-8', $resp->{content});
+
+ #my ($title) = $body =~ m|<em>サブタイトル</em><span>:</span><strong>(.+?)</strong>|;
+ my ($station) = $body =~ m|<em>放送局</em><span>:</span><strong>(.+?)</strong>|;
+ my ($min) = $body =~ m|<em>放送時間/分</em><span>:</span><strong>(\d+?)</strong>|;
+
+ die "詳細情報(放送局・放送時間)が取得できませんでした. detail_url=$detail_url" unless $station && $min;
+
+ return ($station, $min);
+
+ }else{
+ return;
+ }
+
+}
+
+
+
+sub make_basename {
+ my($date, $title) = @_;
+
+ $date =~ s/T\d{2}:\d{2}$//; #時刻部を除去
+
+ my $basename = "${date}_${title}";
+
+ #半角記号類 -> 全角にする
+ for ($basename) {
+ s/\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;
+ }
+
+ $basename = substr($basename, 0, $LIMIT_LENGTH_OF_FILE_BASE_NAME);
+
+ return $basename;
+}
+
+