1st ver.
authorsatomichan <git-ubuntu-n95.20250719@...>
Tue, 26 Aug 2025 15:01:41 +0000 (00:01 +0900)
committersatomichan <git-ubuntu-n95.20250719@...>
Tue, 26 Aug 2025 15:01:41 +0000 (00:01 +0900)
foltia-dl.pl [new file with mode: 0755]

diff --git a/foltia-dl.pl b/foltia-dl.pl
new file mode 100755 (executable)
index 0000000..45be956
--- /dev/null
@@ -0,0 +1,250 @@
+#!/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;
+}
+
+