File Coverage

blib/lib/WWW/FetchStory/Fetcher/FanfictionNet.pm
Criterion Covered Total %
statement 9 222 4.0
branch 0 94 0.0
condition 0 6 0.0
subroutine 3 15 20.0
pod 12 12 100.0
total 24 349 6.8


line stmt bran cond sub pod time code
1             package WWW::FetchStory::Fetcher::FanfictionNet;
2             $WWW::FetchStory::Fetcher::FanfictionNet::VERSION = '0.2602';
3 1     1   209012 use strict;
  1         2  
  1         36  
4 1     1   4 use warnings;
  1         1  
  1         57  
5             =head1 NAME
6              
7             WWW::FetchStory::Fetcher::FanfictionNet - fetching module for WWW::FetchStory
8              
9             =head1 VERSION
10              
11             version 0.2602
12              
13             =head1 DESCRIPTION
14              
15             This is the FanfictionNet story-fetching plugin for WWW::FetchStory.
16              
17             =cut
18              
19 1     1   5 use parent qw(WWW::FetchStory::Fetcher);
  1         5  
  1         7  
20              
21             =head2 info
22              
23             Information about the fetcher.
24              
25             $info = $self->info();
26              
27             =cut
28              
29             sub info {
30 0     0 1   my $self = shift;
31            
32 0           my $info = "(http://www.fantiction.net/) Huge fan fiction archive.";
33              
34 0           return $info;
35             } # info
36              
37             =head2 priority
38              
39             The priority of this fetcher. Fetchers with higher priority
40             get tried first. This is useful where there may be a generic
41             fetcher for a particular site, and then a more specialized fetcher
42             for particular sections of a site. For example, there may be a
43             generic FanfictionNet fetcher, and then refinements for particular
44             FanfictionNet community, such as the sshg_exchange community.
45             This works as either a class function or a method.
46              
47             This must be overridden by the specific fetcher class.
48              
49             $priority = $self->priority();
50              
51             $priority = WWW::FetchStory::Fetcher::priority($class);
52              
53             =cut
54              
55             sub priority {
56 0     0 1   my $class = shift;
57              
58 0           return 1;
59             } # priority
60              
61             =head2 allow
62              
63             If this fetcher can be used for the given URL, then this returns
64             true.
65             This must be overridden by the specific fetcher class.
66              
67             if ($obj->allow($url))
68             {
69             ....
70             }
71              
72             =cut
73              
74             sub allow {
75 0     0 1   my $self = shift;
76 0           my $url = shift;
77              
78 0           return ($url =~ /fanfiction\.net/);
79             } # allow
80              
81             =head1 Private Methods
82              
83             =head2 extract_story
84              
85             Extract the story-content from the fetched content.
86              
87             my ($story, $title) = $self->extract_story(content=>$content,
88             title=>$title);
89              
90             =cut
91              
92             sub extract_story {
93 0     0 1   my $self = shift;
94 0           my %args = (
95             content=>'',
96             title=>'',
97             @_
98             );
99 0           my $content = $args{content};
100              
101 0           my $title = $args{title};
102 0           my $author = '';
103              
104 0 0         if ($content =~ m#<div\s+id=content><center><b>([^<]+)</b>\s*by\s*<a href='/u/\d+/'>([^<]+)</a>#)
105             {
106 0           $title = $1;
107 0           $author = $2;
108             }
109 0           $author =~ s/^\s*//;
110 0           $author =~ s/\s*$//;
111 0 0         warn "title=$title\n" if ($self->{verbose} > 1);
112 0 0         warn "author=$author\n" if ($self->{verbose} > 1);
113              
114 0           my $universe = $self->parse_universe(content=>$content);
115 0 0         warn "universe=$universe\n" if ($self->{verbose} > 1);
116              
117 0           my $chapter = $self->parse_ch_title(%args);
118 0 0         warn "chapter=$chapter\n" if ($self->{verbose} > 1);
119              
120 0           my $story = '';
121 0 0         if ($content =~ m# id='storycontent'\s*>(.*?)\s*<div align=center>#s)
    0          
    0          
    0          
    0          
    0          
    0          
122             {
123 0           $story = $1;
124             }
125             elsif ($content =~ m# id='storycontent'\s*>(.*?)\s*<center>.*?Ch \d+ of <a href='[^']+'>\d+</a>.*?<br>\s*<span class='xbut corner_round'>#s)
126             {
127 0           $story = $1;
128             }
129             elsif ($content =~ m#class='storycontent' id='storycontent'\s*>(.*?)\s*<center>\s*<br>\s*<span class='xbut corner_round'>#s)
130             {
131 0           $story = $1;
132             }
133             elsif ($content =~ m#class='storycontent puretext' id='storycontent'\s*>(.*?)\s*</div>\s*</div>\s*<div id='content' class='puretext'>#s)
134             {
135 0           $story = $1;
136             }
137             elsif ($content =~ m#id=storycontent class=storycontent>(.*?)\s*</div>\s*</div>\s*<div id=content>#s)
138             {
139 0           $story = $1;
140             }
141             elsif ($content =~ m#id=storycontent class=storycontent>(.*?)\s*</div>\s*<div id=content>#s)
142             {
143 0           $story = $1;
144             }
145             elsif ($content =~ m#<div id=storytext class=storytext>(.*?)</div>#s)
146             {
147 0           $story = $1;
148             }
149              
150 0 0         if ($story)
151             {
152 0           $story = $self->tidy_chars($story);
153             }
154             else
155             {
156 0           die "Failed to extract story for $title";
157             }
158              
159 0           my $story_title = "$title: $chapter";
160 0 0         $story_title = $title if ($title eq $chapter);
161 0 0         $story_title = $title if ($chapter eq '');
162              
163 0           my $out = '';
164 0 0         if ($story)
165             {
166 0           $out .= "<h1>$story_title</h1>\n";
167 0 0         $out .= "<p>by $author</p>\n" if $author;
168 0 0         $out .= "<br/>\n<b>Universe:</b> $universe\n" if $universe;
169 0           $out .= "<div>\n";
170 0           $out .= "$story\n";
171 0           $out .= "</div>\n";
172             }
173 0           return ($out, $story_title);
174             } # extract_story
175              
176             =head2 parse_toc
177              
178             Parse the table-of-contents file.
179              
180             %info = $self->parse_toc(content=>$content,
181             url=>$url,
182             urls=>\@urls);
183              
184             This should return a hash containing:
185              
186             =over
187              
188             =item chapters
189              
190             An array of URLs for the chapters of the story. In the case where the
191             story only takes one page, that will be the chapter.
192             In the case where multiple URLs have been passed in, it will be those URLs.
193              
194             =item title
195              
196             The title of the story.
197              
198             =back
199              
200             It may also return additional information, such as Summary.
201              
202             =cut
203              
204             sub parse_toc {
205 0     0 1   my $self = shift;
206 0           my %args = (
207             url=>'',
208             content=>'',
209             @_
210             );
211              
212 0           my %info = ();
213 0           my $content = $args{content};
214              
215 0           my @chapters = ();
216 0           $info{url} = $args{url};
217 0           my $sid='';
218 0 0         if ($args{url} =~ m#https?://www.fanfiction.net/s/(\d+)/#)
219             {
220 0           $sid = $1;
221             }
222             else
223             {
224 0           return $self->SUPER::parse_toc(%args);
225             }
226 0           $info{title} = $self->parse_title(%args);
227 0           my $auth_url = '';
228 0 0         if ($content =~ m#href='(/u/\d+/[-\w]+)'>([^<]+)</a>#s)
229             {
230 0           $auth_url = $1;
231 0           $info{author} = $2;
232 0 0         warn "author from URL=$info{author}\n" if ($self->{verbose} > 1);
233             }
234             else
235             {
236 0           $info{author} = $self->parse_author(%args);
237             }
238             # The summary is on the Author page!
239 0 0 0       if ($auth_url && $sid)
240             {
241 0           my $auth_page = $self->get_page("http://www.fanfiction.net${auth_url}");
242 0 0         if ($auth_page =~ m#<a href="/s/${sid}/\d+/[-\w]+">.*?<div\s*class='[-\w\s]+'>([^<]+)<div#s)
    0          
243             {
244 0           $info{summary} = $1;
245             }
246             elsif ($auth_page =~ m#<a class=reviews href='/r/${sid}/'>reviews</a>\s*<div class='z-indent z-padtop'>([^<]+)<div#s)
247             {
248 0           $info{summary} = $1;
249             }
250             else
251             {
252 0           $info{summary} = $self->parse_summary(%args);
253             }
254             }
255             else
256             {
257 0           $info{summary} = $self->parse_summary(%args);
258             }
259              
260             # get the mobile version of the page in order to parse the other stuff
261 0           my $mob_url = $args{url};
262 0           $mob_url =~ s/www/m/;
263 0           my $mob_page = $self->get_page($mob_url);
264 0           $info{characters} = $self->parse_characters(%args,content=>$mob_page);
265 0           $info{category} = $self->parse_category(%args,content=>$mob_page);
266 0           $info{universe} = $self->parse_universe(%args,content=>$mob_page);
267              
268             # this needs the non-mobile version
269 0           $info{chapters} = $self->parse_chapter_urls(%args,
270             sid=>$sid, mob_url=>$mob_url);
271              
272 0           return %info;
273             } # parse_toc
274              
275             =head2 parse_chapter_urls
276              
277             Figure out the URLs for the chapters of this story.
278              
279             =cut
280             sub parse_chapter_urls {
281 0     0 1   my $self = shift;
282 0           my %args = (
283             url=>'',
284             content=>'',
285             @_
286             );
287 0           my $content = $args{content};
288 0           my $sid = $args{sid};
289 0           my @chapters = ();
290 0 0         if (defined $args{urls})
291             {
292 0           @chapters = @{$args{urls}};
  0            
293 0           for (my $i = 0; $i < @chapters; $i++)
294             {
295 0           $chapters[$i] =~ s/www/m/; # convert to mobile-site URL
296             }
297             }
298              
299 0 0 0       if (@chapters == 0 or @chapters == 1)
300             {
301             # fortunately fanfiction.net has a sane-ish chapter system
302 0 0         if ($content =~ m#-\s+Chapters:\s+(\d+)\s+-\s+Words:\s+[\d,]#is)
303             {
304 0           my $num_chapters = $1;
305 0           @chapters = ();
306 0           my $fmt = $args{url};
307 0           $fmt =~ s/www/m/;
308 0           $fmt =~ s!/\d+/\d+/!/%d/\%d/!;
309 0           for (my $i=1; $i <= $num_chapters; $i++)
310             {
311 0           my $ch_url = sprintf($fmt, $sid, $i);
312 0 0         warn "chapter=$ch_url\n" if ($self->{verbose} > 1);
313 0           push @chapters, $ch_url;
314             }
315             }
316             else
317             {
318 0           @chapters = ($args{mob_url});
319             }
320             }
321              
322 0           return \@chapters;
323             } # parse_chapter_urls
324              
325             =head2 parse_category
326              
327             Get the categories.
328              
329             =cut
330             sub parse_category {
331 0     0 1   my $self = shift;
332 0           my %args = @_;
333              
334 0           my $content = $args{content};
335              
336 0           my $category = '';
337 0           my $characters = '';
338 0 0         if ($content =~ m!Rated:\s*[+\w]+,\s*English,\s*([^,]+),\s*(.*?),\s*Words:!)
    0          
    0          
    0          
339             {
340 0           $category = $1;
341 0           $characters = $2;
342 0           $category =~ s!\s*\&\s!, !g;
343 0           $characters =~ s!\.!!g;
344             }
345             elsif ($content =~ m!Rated:\s*[+\w]+,\s*English,\s*([^,]+),\s*([^,]+),\s*Words:[^<]+!)
346             {
347 0           $category = $1;
348 0           $characters = $2;
349 0           $category =~ s!\s*\&\s!, !g;
350 0           $characters =~ s!\s*\&\s!, !g;
351 0           $characters =~ s!\.!!g;
352             }
353             elsif ($content =~ m!Rated:\s*[+\w]+,\s*English,\s*([^,]+),\s*Words:[^<]+!)
354             {
355             # only one of category or characters
356 0           $category = $1;
357 0 0         if ($category =~ /\./)
358             {
359             # these are characters
360 0           $category = '';
361             }
362             else
363             {
364 0           $category =~ s!\s*\&\s!, !g;
365             }
366             }
367             elsif ($content =~ m!Rated:\s*[\w]+,\s*English,\s*([^,]+),\s*([^,]+),\s*(P:[^<]+)<!)
368             {
369 0           $category = $1;
370 0           $characters = $2;
371 0           $category =~ s!\s*\&\s!, !g;
372 0           $characters =~ s!\s*\&\s!, !g;
373 0           $characters =~ s!\.!!g;
374             }
375             else
376             {
377 0           $category = $self->SUPER::parse_category(%args);
378             }
379 0           return $category;
380             } # parse_category
381              
382             =head2 parse_characters
383              
384             Get the characters.
385              
386             =cut
387             sub parse_characters {
388 0     0 1   my $self = shift;
389 0           my %args = @_;
390              
391 0           my $content = $args{content};
392              
393 0           my $category = '';
394 0           my $characters = '';
395 0 0         if ($content =~ m!Rated:\s*[+\w]+,\s*English,\s*([^,]+),\s*(.*?),\s*Words:!)
    0          
    0          
    0          
396             {
397 0           $category = $1;
398 0           $characters = $2;
399 0           $category =~ s!\s*\&\s!, !g;
400 0           $characters =~ s!\.!!g;
401             }
402             elsif ($content =~ m!Rated:\s*[+\w]+,\s*English,\s*([^,]+),\s*([^,]+),\s*Words:[^<]+!)
403             {
404 0           $category = $1;
405 0           $characters = $2;
406 0           $category =~ s!\s*\&\s!, !g;
407 0           $characters =~ s!\s*\&\s!, !g;
408 0           $characters =~ s!\.!!g;
409             }
410             elsif ($content =~ m!Rated:\s*[+\w]+,\s*English,\s*([^,]+),\s*Words:[^<]+!)
411             {
412             # only one of category or characters
413 0           $characters = $1;
414 0 0         if ($characters =~ /\./)
415             {
416             # these are characters
417 0           $characters =~ s!\s*\&\s!, !g;
418 0           $characters =~ s!\.!!g;
419             }
420             else
421             {
422 0           $characters = '';
423             }
424             }
425             elsif ($content =~ m!Rated:\s*[-\w]+,\s*English,\s*([^,]+),\s*([^,]+),\s*(P:[^<]+)<!)
426             {
427 0           $category = $1;
428 0           $characters = $2;
429 0           $category =~ s!\s*\&\s!, !g;
430 0           $characters =~ s!\s*\&\s!, !g;
431 0           $characters =~ s!\.!!g;
432             }
433             else
434             {
435 0           $characters = $self->SUPER::parse_characters(%args);
436             }
437              
438             # Correct some character names
439 0           $characters =~ s/Hermione G/Hermione Granger/;
440 0           $characters =~ s/Severus S/Severus Snape/;
441 0           $characters =~ s/Harry P/Harry Potter/;
442 0           $characters =~ s/Draco M/Draco Malfoy/;
443 0           $characters =~ s/Remus L/Remus Lupin/;
444 0           $characters =~ s/Sirius B/Sirius Black/;
445 0           $characters =~ s/Alastor M/Alastor Moody/;
446 0           $characters =~ s/Ginny W/Ginny Weasley/;
447 0           $characters =~ s/Fred W/Fred Weasley/;
448 0           $characters =~ s/George W/George Weasley/;
449 0           $characters =~ s/8th Doctor/Eighth Doctor/;
450 0           $characters =~ s/9th Doctor/Ninth Doctor/;
451 0           $characters =~ s/10th Doctor/Tenth Doctor/;
452 0           $characters =~ s/11th Doctor/Eleventh Doctor/;
453 0           $characters =~ s/Rose T/Rose Tyler/;
454 0           $characters =~ s/Donna N/Donna Noble/;
455 0           $characters =~ s/Jenny - Doctor's Daughter/Jenny/;
456 0           $characters =~ s#River Song/Melody P III#River Song#;
457              
458 0           return $characters;
459             } # parse_characters
460              
461             =head2 parse_universe
462              
463             Get the universe.
464              
465             =cut
466             sub parse_universe {
467 0     0 1   my $self = shift;
468 0           my %args = @_;
469              
470 0           my $content = $args{content};
471              
472 0           my $universe = '';
473 0 0         if ($content =~ m!&#187; <a href="/(?:anime|book|cartoon|comic|game|misc|movie|play|tv)/[-\w]+/">([^<]+)</a>!)
474             {
475 0           $universe = $1;
476             }
477             else
478             {
479 0           $universe = $self->SUPER::parse_universe(%args);
480             }
481 0           return $universe;
482             } # parse_universe
483              
484             =head2 parse_author
485              
486             Get the author from the content
487              
488             =cut
489             sub parse_author {
490 0     0 1   my $self = shift;
491 0           my %args = (
492             content=>'',
493             @_
494             );
495              
496 0           my $author = $self->SUPER::parse_author(%args);
497             # extract the actual author name
498 0 0         if ($author =~ m#<a href='/u/\d+/[-\w]+'>([^<]+)</a>#s)
499             {
500 0           $author = $1;
501             }
502              
503 0           return $author;
504             } # parse_author
505              
506             =head2 parse_title
507              
508             Get the title from the content
509              
510             =cut
511             sub parse_title {
512 0     0 1   my $self = shift;
513 0           my %args = (
514             content=>'',
515             @_
516             );
517              
518 0           my $content = $args{content};
519 0           my $title = '';
520 0 0         if ($content =~ m/&#187; <b>([^<]+)<\/b>/s)
    0          
    0          
521             {
522 0           $title = $1;
523             }
524             elsif ($content =~ m#<title>([^<]+)\s*Chapter[^<]+</title>#is)
525             {
526 0           $title = $1;
527             }
528             elsif ($content =~ m#<title>([^<]+), a [^<]+ fanfic [|] FanFiction</title>#is)
529             {
530 0           $title = $1;
531             }
532             else
533             {
534 0           $title = $self->SUPER::parse_title(%args);
535             }
536 0           $title =~ s/\s+$//;
537 0           return $title;
538             } # parse_title
539              
540             =head2 parse_ch_title
541              
542             Get the chapter title from the content
543              
544             =cut
545             sub parse_ch_title {
546 0     0 1   my $self = shift;
547 0           my %args = (
548             url=>'',
549             content=>'',
550             @_
551             );
552              
553 0           my $content = $args{content};
554 0           my $title = '';
555 0 0         if ($content =~ m#^Chapter\s*(\d+:[^<]+)<br#m)
    0          
    0          
    0          
556             {
557 0           $title = $1;
558             }
559             elsif ($content =~ m#<option[^>]+selected>([^<]+)</option>#s)
560             {
561 0           $title = $1;
562             }
563             elsif ($content =~ m#<SELECT title='chapter navigation'.*?<option[^>]+selected>([^<]+)<#s)
564             {
565 0           $title = $1;
566             }
567             elsif ($content =~ m#<title>([^<]+)</title>#is)
568             {
569 0           $title = $1;
570             }
571             else
572             {
573 0           $title = '';
574             }
575 0           $title =~ s/<u>//ig;
576 0           $title =~ s/<\/u>//ig;
577 0           $title =~ s/^Fanfic:\s*//;
578 0           return $title;
579             } # parse_ch_title
580              
581             1; # End of WWW::FetchStory::Fetcher::FanfictionNet
582             __END__