保存先ファイル名: 全角に変換する記号類を追加, 空白は半角 _ に.
[foltia-dl.git] / foltia-dl.pl
1 #!/usr/bin/env -S perl -w
2
3 # foltia-dl.pl   ---- foltia ANIME LOCKER 録画データ ダウンローダ
4 #                                (https://satomichan.jp/foltia-dl)
5 #
6 # foltia ANIME LOCKER (https://foltia.com/ANILOC/) が動作している
7 # Webサイトから録画・録音ファイルをダウンロードし, 
8 # メタデータ(番組名・放送局名・放送時間)を付加して保存するツールです.
9 #
10 # 使い方:
11 #   foltia-dl.pl [--tsv] [--grep <title_regexp_string>]
12 #
13 #   --tsv または -t を指定すると, タブ区切りテキスト出力モードになります.
14 #   指定しないときが通常モードで, シェルで実行可能な文字列を出力します.
15 #   --grep <正規表現文字列> を設定すると, 番組名が<正規表現文字列>に
16 #   合致した場合のみ出力します.
17 #
18 # 実行例:
19 #   foltia-dl.pl --grep 'きょうの(料理|健康)' | bash
20 #
21 # $FOLTIA_HOST を指定している箇所は, 環境に合わせて foltia ANIME LOCKER が
22 # 動作しているホスト名またはIPアドレスに書き換えてください.
23 #
24 #
25 # Copyright 2025 FUKUDA Satomi (https://satomichan.jp/)
26
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
31
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.
35
36 # See the License for the specific language governing permissions and
37 # limitations under the License.
38
39 use strict;
40 use warnings;
41
42 use utf8;
43 binmode STDOUT, ":utf8";
44 binmode STDERR, ":utf8";
45
46 use HTTP::Tiny;
47 use Encode;
48 use HTML::Entities;   # apt install libhtml-parser-perl
49 use Getopt::Long qw(:config posix_default no_ignore_case gnu_compat);
50
51 #要変更箇所 お使いの環境に合わせて書き換えてください.
52 #  (foltia ANIME LOCKER が動作しているホスト名またはIPアドレス)
53 our $FOLTIA_HOST = 'http://FOLTIA-ANIME-LOCKER.test';
54
55 #保存ファイル名(拡張子を除いた部分)の最大長さ(bytes)
56 our $LIMIT_LENGTH_OF_FILE_BASE_NAME = 80;
57
58
59
60 #オプション解析
61 my %opts;
62 GetOptions(  \%opts, ('tsv|t', 'grep=s')  );
63 our ($is_tsv_mode, $is_cmdline_mode, $opts_grep);
64
65 #TSVモード or コマンドラインモード(通常)
66 if ($opts{'tsv'}) {
67     $is_tsv_mode     = 1;
68 }else{
69     $is_cmdline_mode = 1;
70 }
71
72 #番組名grep
73 $opts_grep = Encode::decode('UTF-8', $opts{'grep'});
74
75
76
77 my $cnt_programs = 0;
78 my %used_filename;
79
80 get_rec_table('/recorded/recfiles_all.php');
81
82 print STDERR "ダウンロード対象 番組数:$cnt_programs ファイル数:@{[ 0+ keys %used_filename ]}\n";
83
84 exit;
85
86
87
88 sub get_rec_table {
89     my ($a_path) = @_;
90     my $resp  = HTTP::Tiny->new->get($FOLTIA_HOST. $a_path);
91     
92     if ($resp->{success}) {
93         my $body = Encode::decode('UTF-8', $resp->{content});
94         
95         #レコード(番組)ごとのループ
96         while ($body =~ m|<tr.+?</tr>|sg) {
97             my $rec = $&;
98             $rec =~ s/(<br>)|\n|\r//g;
99             
100             my ($chapter)  = $rec =~ m|<td class="chapter">(.+?)</td>|; #話数
101
102             next unless $chapter;           #話数がなければ番組ではない
103             next if     $rec =~ /watchnow/; #録画中
104             
105             my ($pid)   = $rec =~ m|pid=(-\d+)|;
106             
107             my $date;
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";
110             }
111             
112             my ($title) = $rec =~ m|<td class="subtitle">(.+?)</td>|;
113                 $title  = HTML::Entities::decode_entities($title);  #HTML文字実体参照(&xxxx;) デコード
114
115             my (@paths) = $rec =~ m|<a href='(/tv/[-/\w\.]+?)'>|g;
116             
117             die "日時・番組名が取得できませんでした. rec=$rec" unless $date && $title;
118             
119             #番組名grep
120             if ($opts_grep) {
121                 next unless $title =~ /$opts_grep/;
122             }
123             
124             #ここまできたら, DL対象の番組である
125             $cnt_programs++;
126             
127             #放送局・放送長さ取得
128             my($station, $min) = ('', '');
129             if ($pid) {  #テレビ番組のとき
130                 ($station, $min) = get_detail($pid);
131                 
132             }else{       #ラジオ番組のとき
133                 ($station) = $paths[0] =~ m|-(\w+)\.\w+$|;
134             }
135     
136             #print "($chapter,@{[ $pid ? $pid : '' ]}) $date, $title, $station, $min, {@paths}\n";
137             
138             #ファイル名 基本部
139             my $basename = make_basename($date, $title);
140             
141             
142             #DL対象ファイルごとのループ
143             foreach my $a_path (@paths) {
144             
145                 #拡張子
146                 my ($ext) = $a_path =~ /\.(\w+)$/;
147                 $ext = lc $ext;
148                 
149                 #ファイル名 重複あるとき
150                 $basename .= "_c$chapter" if $used_filename{"$basename.$ext"};
151                 
152                 $used_filename{"$basename.$ext"} = 1;
153                 
154                 if ($is_tsv_mode) {
155                     #TSV出力モード
156                     my @outputs = ("$basename.$ext", $a_path, $title, $station, "$date (${min}min)");
157                     print join("\t", @outputs). "\n";
158                 
159                 } elsif ($is_cmdline_mode) {
160                     #コマンドライン出力モード
161                     print "wget --continue -O '$basename.$ext' '$FOLTIA_HOST$a_path'";
162                 
163                     #.mp4のとき -> メタ情報を付加するコマンドも出力
164                     if ($ext eq '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'";
169                     }
170                     
171                     print "\n";
172                 }
173                 
174                 
175             } #DL対象ファイルごとのループ
176             
177             
178         } #レコード(番組)ごとのループ
179         
180         #次の HTML ページ
181         get_rec_table($1) if $body =~ m|<a rel=next href="(.+?)" >|;
182         
183     } else {
184         die "録画一覧が取得できませんでした. get=$FOLTIA_HOST$a_path";
185     }
186
187 }
188
189
190
191 sub get_detail {
192     my ($pid) = @_;
193     
194     my $detail_url = "${FOLTIA_HOST}/recorded/selectcaptureimage.php?pid=$pid";
195
196     my $resp = HTTP::Tiny->new->get($detail_url);
197     
198     if ($resp->{success}) {
199         my $body = Encode::decode('UTF-8', $resp->{content});
200         
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>|;
204         
205         die "詳細情報(放送局・放送時間)が取得できませんでした. detail_url=$detail_url" unless $station && $min;
206         
207         return ($station, $min);
208     
209     }else{
210         return;
211     }
212
213 }
214
215
216
217 sub make_basename {
218     my($date, $title) = @_;
219
220     $date    =~ s/T\d{2}:\d{2}$//; #時刻部を除去
221     
222     my $basename = "${date}_${title}";
223     
224     #空白 -> _ にする
225     $basename =~ s/\s+/_/g;
226     $basename =~ s/ +/_/g;
227
228     #半角記号類 -> 全角にする
229     for ($basename) {
230         s/"/”/g;
231         s/'/’/g;
232         s/`/`/g;
233         s/,/,/g;
234         s/\././g;
235         s/</</g;
236         s/>/>/g;
237         s/\|/|/g;
238         s/:/:/g;
239         s/;/;/g;
240         s/\?/?/g;
241         s/!/!/g;
242         s/&/&/g;
243         s/%/%/g;
244         s/~/ ̄/g;
245         s/\$/$/g;
246         s/\*/*/g;
247         s/\\/¥/g;
248         s/\////g;
249         s/\././g;
250         s/\+/+/g;
251         s/\(/(/g;
252         s/\)/)/g;
253         s/\[/[/g;
254         s/]/]/g;
255         s/{/{/g;
256         s/}/}/g;
257     }
258     
259     $basename = substr($basename, 0, $LIMIT_LENGTH_OF_FILE_BASE_NAME);
260     
261     return $basename;
262 }
263
264