blib/lib/FlashVideo/Utils.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 47 | 197 | 23.8 |
branch | 6 | 76 | 7.8 |
condition | 5 | 36 | 13.8 |
subroutine | 12 | 30 | 40.0 |
pod | 0 | 19 | 0.0 |
total | 70 | 358 | 19.5 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | # Part of get-flash-videos. See get_flash_videos for copyright. | ||||||
2 | package FlashVideo::Utils; | ||||||
3 | |||||||
4 | 5 | 5 | 52193 | use strict; | |||
5 | 10 | ||||||
5 | 176 | ||||||
5 | 5 | 5 | 25 | no warnings 'uninitialized'; | |||
5 | 7 | ||||||
5 | 165 | ||||||
6 | 5 | 5 | 26 | use base 'Exporter'; | |||
5 | 8 | ||||||
5 | 496 | ||||||
7 | 5 | 5 | 5096 | use HTML::Entities; | |||
5 | 34412 | ||||||
5 | 527 | ||||||
8 | 5 | 5 | 4503 | use HTML::TokeParser; | |||
5 | 30042 | ||||||
5 | 160 | ||||||
9 | 5 | 5 | 5364 | use Encode; | |||
5 | 61704 | ||||||
5 | 513 | ||||||
10 | |||||||
11 | 5 | 5 | 47 | use constant FP_KEY => "Genuine Adobe Flash Player 001"; | |||
5 | 10 | ||||||
5 | 556 | ||||||
12 | 5 | 5 | 28 | use constant EXTENSIONS => qr/\.(?:flv|mp4|mov|wmv|avi|m4v)/i; | |||
5 | 9 | ||||||
5 | 247 | ||||||
13 | 5 | 5 | 24 | use constant MAX_REDIRECTS => 5; | |||
5 | 9 | ||||||
5 | 7252 | ||||||
14 | |||||||
15 | our @EXPORT = qw(debug info error | ||||||
16 | extract_title extract_info title_to_filename get_video_filename url_exists | ||||||
17 | swfhash swfhash_data EXTENSIONS get_user_config_dir get_win_codepage | ||||||
18 | is_program_on_path get_terminal_width json_unescape | ||||||
19 | convert_sami_subtitles_to_srt from_xml); | ||||||
20 | |||||||
21 | sub debug(@) { | ||||||
22 | # Remove some sensitive data | ||||||
23 | 0 | 0 | 0 | 0 | my $string = "@_\n"; | ||
24 | 0 | 0 | $string =~ s/\Q$ENV{HOME}\E/~/g; | ||||
25 | 0 | 0 | 0 | print STDERR $string if $App::get_flash_videos::opt{debug}; | |||
26 | } | ||||||
27 | |||||||
28 | sub info(@) { | ||||||
29 | 0 | 0 | 0 | 0 | 0 | print STDERR "@_\n" unless $App::get_flash_videos::opt{quiet}; | |
30 | } | ||||||
31 | |||||||
32 | sub error(@) { | ||||||
33 | 0 | 0 | 0 | 0 | print STDERR "@_\n"; | ||
34 | } | ||||||
35 | |||||||
36 | sub extract_title { | ||||||
37 | 0 | 0 | 0 | 0 | my($browser) = @_; | ||
38 | 0 | 0 | return extract_info($browser)->{title}; | ||||
39 | } | ||||||
40 | |||||||
41 | sub extract_info { | ||||||
42 | 0 | 0 | 0 | 0 | my($browser) = @_; | ||
43 | 0 | 0 | my($title, $meta_title); | ||||
44 | |||||||
45 | 0 | 0 | my $p = HTML::TokeParser->new(\$browser->content); | ||||
46 | 0 | 0 | while(my $token = $p->get_tag("title", "meta")) { | ||||
47 | 0 | 0 | my($tag, $attr) = @$token; | ||||
48 | |||||||
49 | 0 | 0 | 0 | 0 | if($tag eq 'meta' && $attr->{name} =~ /title/i) { | ||
0 | |||||||
50 | 0 | 0 | $meta_title = $attr->{content}; | ||||
51 | } elsif($tag eq 'title') { | ||||||
52 | 0 | 0 | $title = $p->get_trimmed_text; | ||||
53 | } | ||||||
54 | } | ||||||
55 | |||||||
56 | return { | ||||||
57 | 0 | 0 | title => $title, | ||||
58 | meta_title => $meta_title, | ||||||
59 | }; | ||||||
60 | } | ||||||
61 | |||||||
62 | sub swfhash { | ||||||
63 | 0 | 0 | 0 | 0 | my($browser, $url) = @_; | ||
64 | |||||||
65 | 0 | 0 | $browser->get($url); | ||||
66 | |||||||
67 | 0 | 0 | return swfhash_data($browser->content, $url); | ||||
68 | } | ||||||
69 | |||||||
70 | sub swfhash_data { | ||||||
71 | 0 | 0 | 0 | 0 | my ($data, $url) = @_; | ||
72 | |||||||
73 | die "Must have Compress::Zlib and Digest::SHA for this RTMP download\n" | ||||||
74 | 0 | 0 | 0 | unless eval { | |||
75 | 0 | 0 | require Compress::Zlib; | ||||
76 | 0 | 0 | require Digest::SHA; | ||||
77 | }; | ||||||
78 | |||||||
79 | 0 | 0 | $data = "F" . substr($data, 1, 7) | ||||
80 | . Compress::Zlib::uncompress(substr $data, 8); | ||||||
81 | |||||||
82 | return | ||||||
83 | 0 | 0 | swfsize => length $data, | ||||
84 | swfhash => Digest::SHA::hmac_sha256_hex($data, FP_KEY), | ||||||
85 | swfUrl => $url; | ||||||
86 | } | ||||||
87 | |||||||
88 | sub url_exists { | ||||||
89 | 0 | 0 | 0 | 0 | my($browser, $url) = @_; | ||
90 | |||||||
91 | 0 | 0 | $browser->head($url); | ||||
92 | 0 | 0 | my $response = $browser->response; | ||||
93 | 0 | 0 | debug "Exists on $url: " . $response->code; | ||||
94 | 0 | 0 | 0 | return $url if $response->code == 200; | |||
95 | |||||||
96 | 0 | 0 | my $redirects = 0; | ||||
97 | 0 | 0 | 0 | while ( ($response->code =~ /^30\d/) and ($response->header('Location')) | |||
0 | |||||||
98 | and ($redirects < MAX_REDIRECTS) ) { | ||||||
99 | 0 | 0 | $url = URI->new_abs($response->header('Location'), $url); | ||||
100 | 0 | 0 | $response = $browser->head($url); | ||||
101 | 0 | 0 | debug "Redirected to $url (" . $response->code . ")"; | ||||
102 | 0 | 0 | 0 | if ($response->code == 200) { | |||
103 | 0 | 0 | return $url; | ||||
104 | } | ||||||
105 | 0 | 0 | $redirects++; | ||||
106 | } | ||||||
107 | |||||||
108 | 0 | 0 | return ''; | ||||
109 | } | ||||||
110 | |||||||
111 | sub title_to_filename { | ||||||
112 | 17 | 17 | 0 | 9922 | my($title, $type) = @_; | ||
113 | |||||||
114 | # Extract the extension if we're passed a URL. | ||||||
115 | 17 | 100 | 100 | 32 | if($title =~ s/(@{[EXTENSIONS]})$//) { | ||
17 | 100 | 250 | |||||
116 | 6 | 17 | $type = substr $1, 1; | ||||
117 | } elsif ($type && $type !~ /^\w+$/) { | ||||||
118 | 6 | 31 | $type = substr((URI->new($type)->path =~ /(@{[EXTENSIONS]})$/)[0], 1); | ||||
6 | 11394 | ||||||
119 | } | ||||||
120 | |||||||
121 | 17 | 100 | 106 | $type ||= "flv"; | |||
122 | |||||||
123 | # We want \w below to match non-ASCII characters. | ||||||
124 | 17 | 43 | utf8::upgrade($title); | ||||
125 | |||||||
126 | # Some sites have double-encoded entities, so handle this | ||||||
127 | 17 | 50 | 52 | if ($title =~ /&(?:\w+|#(?:\d+|x[A-F0-9]+));/) { | |||
128 | # Double-encoded - decode again | ||||||
129 | 0 | 0 | $title = decode_entities($title); | ||||
130 | } | ||||||
131 | |||||||
132 | 17 | 127 | $title =~ s/\s+/_/g; | ||||
133 | 17 | 61 | $title =~ s/[^\w\-,()&]/_/g; | ||||
134 | 17 | 100 | $title =~ s/^_+|_+$//g; # underscores at the start and end look bad | ||||
135 | |||||||
136 | # If we have nothing then return a filestamped filename. | ||||||
137 | 17 | 50 | 37 | return get_video_filename($type) unless $title; | |||
138 | |||||||
139 | 17 | 73 | return "$title.$type"; | ||||
140 | } | ||||||
141 | |||||||
142 | sub get_video_filename { | ||||||
143 | 0 | 0 | 0 | my($type) = @_; | |||
144 | 0 | 0 | $type ||= "flv"; | ||||
145 | 0 | return "video" . get_timestamp_in_iso8601_format() . "." . $type; | |||||
146 | } | ||||||
147 | |||||||
148 | sub get_timestamp_in_iso8601_format { | ||||||
149 | 5 | 5 | 4641 | use Time::localtime; | |||
5 | 46819 | ||||||
5 | 1212 | ||||||
150 | 0 | 0 | 0 | my $time = localtime; | |||
151 | 0 | return sprintf("%04d%02d%02d%02d%02d%02d", | |||||
152 | $time->year + 1900, $time->mon + 1, | ||||||
153 | $time->mday, $time->hour, $time->min, $time->sec); | ||||||
154 | } | ||||||
155 | |||||||
156 | sub get_vlc_exe_from_registry { | ||||||
157 | 0 | 0 | 0 | 0 | if ($^O !~ /MSWin/i) { | ||
158 | 0 | die "Doesn't make sense to call this except on Windows"; | |||||
159 | } | ||||||
160 | |||||||
161 | 0 | my $HAS_WIN32_REGISTRY = eval { require Win32::Registry }; | |||||
0 | |||||||
162 | |||||||
163 | 0 | 0 | die "Win32::Registry required for JustWorks(tm) playing on Windows" | ||||
164 | unless $HAS_WIN32_REGISTRY; | ||||||
165 | |||||||
166 | 0 | require Win32::Registry; | |||||
167 | |||||||
168 | # This module, along with Win32::TieRegistry, is horrible and primarily | ||||||
169 | # works by exporting various symbols into the calling package. | ||||||
170 | # Win32::TieRegistry does not offer an easy way of getting the $Registry | ||||||
171 | # object if you require the module rather than use-ing it. | ||||||
172 | 0 | Win32::Registry->import(); | |||||
173 | |||||||
174 | # Ignoring the fact that polluting your caller's namespace is bad | ||||||
175 | # practice, it's also evil because I now have to disable strict so that | ||||||
176 | # Perl won't complain that $HKEY_LOCAL_MACHINE which is exported into my | ||||||
177 | # package at runtime doesn't exist. | ||||||
178 | 0 | my $local_machine; | |||||
179 | |||||||
180 | { | ||||||
181 | 5 | 5 | 51 | no strict 'vars'; | |||
5 | 13 | ||||||
5 | 15757 | ||||||
0 | |||||||
182 | 0 | $local_machine = $::HKEY_LOCAL_MACHINE; | |||||
183 | } | ||||||
184 | |||||||
185 | 0 | my $key = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'; | |||||
186 | |||||||
187 | 0 | $local_machine->Open($key, my $reg); | |||||
188 | |||||||
189 | # Believe it or not, this is Perl, not C | ||||||
190 | 0 | my @applications; | |||||
191 | 0 | $reg->GetKeys(\@applications); | |||||
192 | |||||||
193 | 0 | my $vlc_binary; | |||||
194 | |||||||
195 | 0 | foreach my $application (@applications) { | |||||
196 | 0 | 0 | next unless $application =~ /VLC Media Player/i; | ||||
197 | |||||||
198 | 0 | $reg->Open($application, my $details); | |||||
199 | |||||||
200 | 0 | my %app_properties; | |||||
201 | 0 | $details->GetValues(\%app_properties); | |||||
202 | |||||||
203 | # These values are arrayrefs with value name, type and data. data is | ||||||
204 | # what we care about. | ||||||
205 | 0 | 0 | if ($app_properties{DisplayIcon}->[-1] =~ /\.exe$/i) { | ||||
206 | # Assume this is the VLC executable | ||||||
207 | 0 | $vlc_binary = $app_properties{DisplayIcon}->[-1]; | |||||
208 | 0 | last; | |||||
209 | } | ||||||
210 | } | ||||||
211 | |||||||
212 | 0 | return $vlc_binary; | |||||
213 | } | ||||||
214 | |||||||
215 | sub get_win_codepage { | ||||||
216 | 0 | 0 | 0 | require Win32::API; | |||
217 | |||||||
218 | # Hack for older versions of Win32::API::Type (which Win32::API->import | ||||||
219 | # uses to parse prototypes) to avoid "unknown output parameter type" | ||||||
220 | # warning. Older versions of this module have an INIT block for reading | ||||||
221 | # type information from the DATA filehandle. This doesn't get called when | ||||||
222 | # we require the module rather than use-ing it. More recent versions of | ||||||
223 | # the module don't bother with an INIT block, and instead just have the | ||||||
224 | # initialisation code at package level. | ||||||
225 | 0 | 0 | if (! %Win32::API::Type::Known) { | ||||
226 | 0 | %Win32::API::Type::Known = (int => 'i'); | |||||
227 | } | ||||||
228 | |||||||
229 | 0 | Win32::API->Import("kernel32", "int GetACP()"); | |||||
230 | 0 | return "cp" . GetACP(); | |||||
231 | } | ||||||
232 | |||||||
233 | # Returns a path to the user's configuration data and/or plugins directory. | ||||||
234 | sub get_user_config_dir { | ||||||
235 | # On Windows, use "Application Data" and "get_flash_videos". On other | ||||||
236 | # platforms, use the user's home directory (specified by the HOME | ||||||
237 | # environment variable) and ".get_flash_videos". Note that on Windows, | ||||||
238 | # the directory has no . prefix as historically, Windows and Windows | ||||||
239 | # applications tend to make dealing with such directories awkward. | ||||||
240 | |||||||
241 | # Note that older versions of Windows don't set an APPDATA environment | ||||||
242 | # variable. | ||||||
243 | |||||||
244 | 0 | 0 | 0 | 0 | 0 | return $^O =~ /MSWin/i ? ($ENV{APPDATA} || 'c:/windows/application data') | |
245 | . "/get_flash_videos" | ||||||
246 | : "$ENV{HOME}/.get_flash_videos"; | ||||||
247 | } | ||||||
248 | |||||||
249 | # Is the specified program on the system PATH? | ||||||
250 | sub is_program_on_path { | ||||||
251 | 0 | 0 | 0 | my($program) = @_; | |||
252 | 0 | my $win = $^O =~ /MSWin/i; | |||||
253 | |||||||
254 | 0 | 0 | for my $dir(split($win ? ";" : ":", $ENV{PATH})) { | ||||
255 | 0 | 0 | return 1 if -f "$dir/$program" . ($win ? ".exe" : ""); | ||||
0 | |||||||
256 | } | ||||||
257 | 0 | return 0; | |||||
258 | } | ||||||
259 | |||||||
260 | sub get_terminal_width { | ||||||
261 | 0 | 0 | 0 | 0 | 0 | if(eval { require Term::ReadKey } && (my($width) = Term::ReadKey::GetTerminalSize())) { | |
0 | 0 | ||||||
262 | 0 | 0 | return $width - 1 if $^O =~ /MSWin|cygwin/i; # seems to be off by 1 on Windows | ||||
263 | 0 | return $width; | |||||
264 | } elsif($ENV{COLUMNS}) { | ||||||
265 | 0 | return $ENV{COLUMNS}; | |||||
266 | } else { | ||||||
267 | 0 | return 80; | |||||
268 | } | ||||||
269 | } | ||||||
270 | |||||||
271 | # Maybe should use a proper JSON parser, but want to avoid the dependency for now.. | ||||||
272 | # (There is now one in FlashVideo::JSON, so consider that -- this is just here | ||||||
273 | # until we have a chance to fix things using it). | ||||||
274 | sub json_unescape { | ||||||
275 | 0 | 0 | 0 | my($s) = @_; | |||
276 | |||||||
277 | 0 | $s =~ s/\\u([0-9a-f]{1,4})/chr hex $1/ge; | |||||
0 | |||||||
278 | 0 | $s =~ s{(\\[\\/rnt"])}{"\"$1\""}gee; | |||||
0 | |||||||
279 | 0 | return $s; | |||||
280 | } | ||||||
281 | |||||||
282 | sub convert_sami_subtitles_to_srt { | ||||||
283 | 0 | 0 | 0 | my ($sami_subtitles, $filename, $decrypt_callback) = @_; | |||
284 | |||||||
285 | 0 | 0 | die "SAMI subtitles must be provided" unless $sami_subtitles; | ||||
286 | 0 | 0 | die "Output SRT filename must be provided" unless $filename; | ||||
287 | |||||||
288 | # Use regexes to "parse" SAMI since HTML::TokeParser is too awkward. It | ||||||
289 | # makes it hard to preserve linebreaks and other formatting in subtitles. | ||||||
290 | # It's also quite slow. | ||||||
291 | 0 | $sami_subtitles =~ s/[\r\n]//g; # flatten | |||||
292 | |||||||
293 | 0 | my @lines = split / | |||||
294 | 0 | shift @lines; # Skip headers | |||||
295 | |||||||
296 | 0 | my @subtitles; | |||||
297 | 0 | my $count = 0; | |||||
298 | |||||||
299 | 0 | my $last_proper_sub_end_time = ''; | |||||
300 | |||||||
301 | 0 | for (@lines) { | |||||
302 | 0 | my ($begin, $sub); | |||||
303 | # Remove span elements | ||||||
304 | 0 | s|<\/?span.*?>| |g; | |||||
305 | |||||||
306 | # replace "&" with "&" | ||||||
307 | 0 | s|&|&|g; | |||||
308 | |||||||
309 | # replace " " with " " | ||||||
310 | 0 | s{&(?:nbsp|#160);}{ }g; | |||||
311 | |||||||
312 | # Start="2284698"> I won't have to drink it |
||||||
313 | #($begin, $sub) = ($1, $2) if m{.*Start="(.+?)".+ |
||||||
314 | |||||||
315 | 0 | 0 | ($begin, $sub) = ($1, $2) if m{[^>]*Start="(.+?)"[^>]*>(.*?)<\/Sync>}i; | ||||
316 | |||||||
317 | 0 | 0 | if (/^\s*Encrypted="true"\s*/i) { | ||||
318 | 0 | 0 | 0 | if ($decrypt_callback and ref($decrypt_callback) eq 'CODE') { | |||
319 | 0 | $sub = $decrypt_callback->($sub); | |||||
320 | } | ||||||
321 | } | ||||||
322 | |||||||
323 | 0 | $sub =~ s@&@&@g; | |||||
324 | 0 | $sub =~ s@(?:?span[^>]*>| | )@ @g; | |||||
325 | |||||||
326 | # Do some tidying up. | ||||||
327 | # Note only tags are removed-- tags are left in place since VLC |
||||||
328 | # and others support this for formatting. | ||||||
329 | 0 | $sub =~ s{?P[^>]*?>}{}g; # remove and similar |
|||||
330 | |||||||
331 | # VLC is very sensitive to tag case. | ||||||
332 | 0 | $sub =~ s{<(/)?([BI])>}{"<$1" . lc($2) . ">"}eg; | |||||
0 | |||||||
333 | |||||||
334 | 0 | decode_entities($sub); # in void context, this works in place | |||||
335 | |||||||
336 | 0 | 0 | 0 | if ($sub and ($begin or $begin == 0)) { | |||
0 | |||||||
337 | # Convert milliseconds into HH:MM:ss,mmm format | ||||||
338 | 0 | my $seconds = int( $begin / 1000.0 ); | |||||
339 | 0 | my $ms = $begin - ( $seconds * 1000.0 ); | |||||
340 | 0 | $begin = sprintf("%02d:%02d:%02d,%03d", (gmtime($seconds))[2,1,0], $ms ); | |||||
341 | |||||||
342 | # Don't strip simple HTML like - VLC and other players | ||||||
343 | # support basic subtitle styling, see: | ||||||
344 | # http://git.videolan.org/?p=vlc.git;a=blob;f=modules/codec/subtitles/subsdec.c | ||||||
345 | |||||||
346 | # Leading/trailing spaces | ||||||
347 | 0 | $sub =~ s/^\s*(.*?)\s*$/$1/; | |||||
348 | |||||||
349 | # strip multispaces | ||||||
350 | 0 | $sub =~ s/\s{2,}/ /g; | |||||
351 | |||||||
352 | # Replace (and similar) with \n. VLC handles \n in SubRip files |
||||||
353 | # fine. For it is case and slash sensitive. |
||||||
354 | 0 | $sub =~ s| |\n|ig; |
|||||
355 | |||||||
356 | 0 | $sub =~ s/^\s*|\s*$//mg; | |||||
357 | |||||||
358 | 0 | 0 | 0 | if ($count and !$subtitles[$count - 1]->{end}) { | |||
359 | 0 | $subtitles[$count - 1]->{end} = $begin; | |||||
360 | } | ||||||
361 | |||||||
362 | # SAMI subtitles are a bit crap. Only a start time is specified for | ||||||
363 | # each subtitle. No end time is specified, so the subtitle is displayed | ||||||
364 | # until the next subtitle is ready to be shown. This means that if | ||||||
365 | # subtitles aren't meant to be shown for part of the video, a dummy | ||||||
366 | # subtitle (usually just a space) has to be inserted. | ||||||
367 | 0 | 0 | 0 | if (!$sub or $sub =~ /^\s+$/) { | |||
368 | 0 | 0 | if ($count) { | ||||
369 | 0 | $last_proper_sub_end_time = $subtitles[$count - 1]->{end}; | |||||
370 | } | ||||||
371 | |||||||
372 | # Gap in subtitles. | ||||||
373 | 0 | next; # this is not a meaningful subtitle | |||||
374 | } | ||||||
375 | |||||||
376 | 0 | push @subtitles, { | |||||
377 | start => $begin, | ||||||
378 | text => $sub, | ||||||
379 | }; | ||||||
380 | |||||||
381 | 0 | $count++; | |||||
382 | } | ||||||
383 | } | ||||||
384 | |||||||
385 | # Ensure the end time for the last subtitle is correct. | ||||||
386 | 0 | $subtitles[$count - 1]->{end} = $last_proper_sub_end_time; | |||||
387 | |||||||
388 | # Write subtitles | ||||||
389 | 0 | 0 | open my $subtitle_fh, '>', $filename | ||||
390 | or die "Can't open subtitles file $filename: $!"; | ||||||
391 | |||||||
392 | # Set filehandle to UTF-8 to avoid "wide character in print" warnings. | ||||||
393 | # Note this does *not* double-encode data as UTF-8 (verify with hexdump). | ||||||
394 | # As per the documentation for binmode: ":utf8 just marks the data as | ||||||
395 | # UTF-8 without further checking". This will cause mojibake if | ||||||
396 | # ISO-8859-1/Latin1 and UTF-8 and are mixed in the same file though. | ||||||
397 | 0 | binmode $subtitle_fh, ':utf8'; | |||||
398 | |||||||
399 | 0 | $count = 1; | |||||
400 | |||||||
401 | 0 | foreach my $subtitle (@subtitles) { | |||||
402 | 0 | print $subtitle_fh "$count\n$subtitle->{start} --> $subtitle->{end}\n" . | |||||
403 | "$subtitle->{text}\n\n"; | ||||||
404 | 0 | $count++; | |||||
405 | } | ||||||
406 | |||||||
407 | 0 | close $subtitle_fh; | |||||
408 | |||||||
409 | 0 | return 1; | |||||
410 | } | ||||||
411 | |||||||
412 | sub from_xml { | ||||||
413 | 0 | 0 | 0 | my($xml, @args) = @_; | |||
414 | |||||||
415 | 0 | 0 | if(!eval { require XML::Simple && XML::Simple::XMLin(" |
||||
0 | 0 | ||||||
416 | 0 | die "Must have XML::Simple to download " . caller =~ /::([^:])+$/ . " videos\n"; | |||||
417 | } | ||||||
418 | |||||||
419 | 0 | $xml = eval { | |||||
420 | 0 | 0 | XML::Simple::XMLin(ref $xml eq 'SCALAR' ? $xml | ||||
0 | |||||||
421 | : ref $xml ? $xml->content | ||||||
422 | : $xml, @args); | ||||||
423 | }; | ||||||
424 | |||||||
425 | 0 | 0 | if($@) { | ||||
426 | 0 | die "$@ (from ", join("::", caller), ")\n"; | |||||
427 | } | ||||||
428 | |||||||
429 | 0 | return $xml; | |||||
430 | } | ||||||
431 | |||||||
432 | 1; |