1 package NonameTV
::Exporter
::Json
;
13 use NonameTV
::Exporter
;
14 use NonameTV
::Language qw
/LoadLanguage/;
15 use NonameTV qw
/norm/;
17 use NonameTV
::Log qw
/d p w StartLogSection EndLogSection SetVerbosity/;
19 use base
'NonameTV::Exporter';
23 Export data in json format.
28 Show which datafiles are created.
31 Show only fatal errors.
34 Print a list of all channels in xml-format to stdout.
37 Remove any old xmltv files from the output directory.
40 Recreate all output files, not only the ones where data has
47 my $class = ref($proto) || $proto;
48 my $self = $class->SUPER::new
( @_ );
49 bless ($self, $class);
51 defined( $self->{Root
} ) or die "You must specify Root";
52 defined( $self->{Language
} ) or die "You must specify Language";
54 $self->{MaxDays
} = 365 unless defined $self->{MaxDays
};
55 $self->{MinDays
} = $self->{MaxDays
} unless defined $self->{MinDays
};
57 $self->{LastRequiredDate
} =
58 DateTime
->today->add( days
=> $self->{MinDays
}-1 )->ymd("-");
60 $self->{OptionSpec
} = [ qw
/export
-channels remove
-old force
-export
61 verbose
+ quiet
+ help
/ ];
63 $self->{OptionDefaults
} = {
64 'export-channels' => 0,
72 my $ds = $self->{datastore
};
74 # Load language strings
75 $self->{lngstr
} = LoadLanguage
( $self->{Language
},
76 "exporter-xmltv", $ds );
88 Export data
in json
-format with one file per day
and channel
.
93 Generate an json
-file listing all channels
and their corresponding
97 Remove all data
-files
for dates that have already passed
.
100 Export all data
. Default is to only export data
for batches that
101 have changed since the
last export
.
108 SetVerbosity
( $p->{verbose
}, $p->{quiet
} );
110 StartLogSection
( "Json", 0 );
112 if( $p->{'export-channels'} )
114 $self->ExportChannelList();
118 if( $p->{'remove-old'} )
125 my $update_started = time();
126 my $last_update = $self->ReadState();
128 if( $p->{'force-export'} ) {
129 $self->FindAll( $todo );
132 $self->FindUpdated( $todo, $last_update );
133 $self->FindUnexportedDays( $todo, $last_update );
136 $self->ExportData( $todo );
138 $self->WriteState( $update_started );
139 EndLogSection
( "Json" );
143 # Find all dates for each channel
148 my $ds = $self->{datastore
};
150 my ( $res, $channels ) = $ds->sa->Sql(
151 "select id from channels where export=1");
153 my $last_date = DateTime
->today->add( days
=> $self->{MaxDays
} -1 );
154 my $first_date = DateTime
->today;
156 while( my $data = $channels->fetchrow_hashref() ) {
157 add_dates
( $todo, $data->{id
},
158 '1970-01-01 00:00:00', '2100-12-31 23:59:59',
159 $first_date, $last_date );
165 # Find all dates that may have new data for each channel.
168 my( $todo, $last_update ) = @_;
170 my $ds = $self->{datastore
};
172 my ( $res, $update_batches ) = $ds->sa->Sql( << 'EOSQL'
173 select channel_id
, batch_id
,
174 min
(start_time
)as min_start
, max
(start_time
) as max_start
177 select id from batches where last_update
> ?
179 group by channel_id
, batch_id
184 my $last_date = DateTime
->today->add( days
=> $self->{MaxDays
} -1 );
185 my $first_date = DateTime
->today;
187 while( my $data = $update_batches->fetchrow_hashref() ) {
188 add_dates
( $todo, $data->{channel_id
},
189 $data->{min_start
}, $data->{max_start
},
190 $first_date, $last_date );
193 $update_batches->finish();
196 # Find all dates that should be exported but haven't been exported
198 sub FindUnexportedDays
{
200 my( $todo, $last_update ) = @_;
202 my $ds = $self->{datastore
};
204 my $days = int( time()/(24*60*60) ) - int( $last_update/(24*60*60) );
205 $days = $self->{MaxDays
} if $days > $self->{MaxDays
};
208 # The previous export was done $days ago.
210 my $last_date = DateTime
->today->add( days
=> $self->{MaxDays
} -1 );
211 my $first_date = $last_date->clone->subtract( days
=> $days-1 );
213 my ( $res, $channels ) = $ds->sa->Sql(
214 "select id from channels where export=1");
216 while( my $data = $channels->fetchrow_hashref() ) {
217 add_dates
( $todo, $data->{id
},
218 '1970-01-01 00:00:00', '2100-12-31 23:59:59',
219 $first_date, $last_date );
230 my $ds = $self->{datastore
};
232 foreach my $channel (keys %{$todo}) {
233 my $chd = $ds->sa->Lookup( "channels", { id
=> $channel } );
235 foreach my $date (sort keys %{$todo->{$channel}}) {
236 $self->ExportFile( $chd, $date );
244 my $ds = $self->{datastore
};
246 my $last_update = $ds->sa->Lookup( 'state', { name
=> "json_last_update" },
249 if( not defined( $last_update ) )
251 $ds->sa->Add( 'state', { name
=> "json_last_update", value
=> 0 } );
260 my( $update_started ) = @_;
262 my $ds = $self->{datastore
};
264 $ds->sa->Update( 'state', { name
=> "json_last_update" },
265 { value
=> $update_started } );
268 #######################################################
273 my( $h, $chid, $from, $to, $first, $last ) = @_;
275 my $from_dt = create_dt
( $from, 'UTC' )->truncate( to
=> 'day' );
276 my $to_dt = create_dt
( $to, 'UTC' )->truncate( to
=> 'day' );
278 $to_dt = $last->clone() if $last < $to_dt;
279 $from_dt = $first->clone() if $first > $from_dt;
281 my $first_dt = $from_dt->clone()->subtract( days
=> 1 );
283 for( my $dt = $first_dt->clone();
284 $dt <= $to_dt; $dt->add( days
=> 1 ) ) {
285 $h->{$chid}->{$dt->ymd('-')} = 1;
291 my( $str, $tz ) = @_;
293 my( $year, $month, $day, $hour, $minute, $second ) =
294 ( $str =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/ );
296 if( defined( $second ) ) {
297 return DateTime
->new(
307 ( $year, $month, $day ) =
308 ( $str =~ /^(\d{4})-(\d{2})-(\d{2})$/ );
310 die( "Xmltv: Unknown time format $str" )
313 return DateTime
->new(
320 #######################################################
322 # Json-specific methods.
327 my( $chd, $date ) = @_;
329 my $section = "Json $chd->{xmltvid}_$date";
331 StartLogSection
( $section, 0 );
335 my $startdate = $date;
336 my $enddate = create_dt
( $date, 'UTC' )->add( days
=> 1 )->ymd('-');
338 my( $res, $sth ) = $self->{datastore
}->sa->Sql( "
339 SELECT * from programs
340 WHERE (channel_id = ?)
341 and (start_time >= ?)
343 ORDER BY start_time",
344 [$chd->{id
}, "$startdate 00:00:00", "$enddate 23:59:59"] );
346 my $w = $self->CreateWriter( $chd, $date );
350 my $d1 = $sth->fetchrow_hashref();
352 if( (not defined $d1) or ($d1->{start_time
} gt "$startdate 23:59:59") ) {
353 $self->CloseWriter( $w );
355 EndLogSection
( $section );
359 while( my $d2 = $sth->fetchrow_hashref() )
361 if( (not defined( $d1->{end_time
})) or
362 ($d1->{end_time
} eq "0000-00-00 00:00:00") )
364 # Fill in missing end_time on the previous entry with the start-time
365 # of the current entry
366 $d1->{end_time
} = $d2->{start_time
}
368 elsif( $d1->{end_time
} gt $d2->{start_time
} )
370 # The previous programme ends after the current programme starts.
371 # Adjust the end_time of the previous programme.
372 w
"Adjusted endtime $d1->{end_time} => $d2->{start_time}";
374 $d1->{end_time
} = $d2->{start_time
}
378 $self->WriteEntry( $w, $d1, $chd )
379 unless $d1->{title
} eq "end-of-transmission";
381 if( $d2->{start_time
} gt "$startdate 23:59:59" ) {
390 # The loop exited because we ran out of data. This means that
391 # there is no data for the day after the day that we
392 # wanted to export. Make sure that we write out the last entry
393 # if we know the end-time for it.
394 if( (defined( $d1->{end_time
})) and
395 ($d1->{end_time
} ne "0000-00-00 00:00:00") )
397 $self->WriteEntry( $w, $d1, $chd )
398 unless $d1->{title
} eq "end-of-transmission";
402 w
"Missing end-time for last entry"
403 unless $date gt $self->{LastRequiredDate
};
407 $self->CloseWriter( $w );
410 EndLogSection
( $section );
416 my( $chd, $date ) = @_;
418 my $xmltvid = $chd->{xmltvid
};
420 my $path = $self->{Root
};
421 my $filename = $xmltvid . "_" . $date . ".js";
423 $self->{writer_filename
} = $filename;
424 $self->{writer_entries
} = 0;
425 # Make sure that writer_entries is always true if we don't require data
427 $self->{writer_entries
} = "0 but true"
428 if( ($date gt $self->{LastRequiredDate
}) or $chd->{empty_ok
} );
440 my $path = $self->{Root
};
441 my $filename = $self->{writer_filename
};
442 delete $self->{writer_filename
};
444 open( my $fh, ">$path$filename.new")
445 or die( "Json: cannot write to $path$filename.new" );
453 my $js = JSON
::XS
->new;
456 $fh->print( $js->encode( $odata ) );
459 system("gzip -f -n $path$filename.new");
460 if( -f
"$path$filename.gz" )
462 system("diff $path$filename.new.gz $path$filename.gz > /dev/null");
465 move
( "$path$filename.new.gz", "$path$filename.gz" );
467 if( not $self->{writer_entries
} )
469 w
"Created empty file";
474 unlink( "$path$filename.new.gz" );
479 move
( "$path$filename.new.gz", "$path$filename.gz" );
481 if( not $self->{writer_entries
} )
491 my( $data, $entry, $chd ) = @_;
493 $self->{writer_entries
}++;
495 my $start_time = create_dt
( $entry->{start_time
}, "UTC" );
496 my $end_time = create_dt
( $entry->{end_time
}, "UTC" );
499 channel
=> $chd->{xmltvid
},
500 start
=> $start_time->strftime( "%s" ),
501 stop
=> $end_time->strftime( "%s" ),
502 title
=> { $chd->{sched_lang
}, $entry->{title
} }
505 $d->{desc
} = { $chd->{sched_lang
} => $entry->{description
} }
506 if defined( $entry->{description
} ) and $entry->{description
} ne "";
508 $d->{'subTitle'} = { $chd->{sched_lang
} => $entry->{subtitle
} }
509 if defined( $entry->{subtitle
} ) and $entry->{subtitle
} ne "";
511 if( defined( $entry->{episode
} ) and ($entry->{episode
} =~ /\S/) )
513 my( $season, $ep, $part );
515 if( $entry->{episode
} =~ /\./ )
517 ( $season, $ep, $part ) = split( /\s*\.\s*/, $entry->{episode
} );
518 if( $season =~ /\S/ )
525 print "Simple episode '$entry->{episode}'\n";
526 $ep = $entry->{episode
};
530 my( $ep_nr, $ep_max ) = split( "/", $ep );
533 my $ep_text = $self->{lngstr
}->{episode_number
} . " $ep_nr";
534 $ep_text .= " " . $self->{lngstr
}->{of
} . " $ep_max"
536 $ep_text .= " " . $self->{lngstr
}->{episode_season
} . " $season"
539 $d->{'episodeNum'} = { xmltv_ns
=> norm
($entry->{episode
}),
540 onscreen
=> $ep_text };
543 # This episode is only a segment and not a real episode.
545 $d->{'episodeNum'} = { xmltv_ns
=> norm
($entry->{episode
}) };
549 if( defined( $entry->{program_type
} ) and ($entry->{program_type
} =~ /\S/) )
551 push @
{$d->{category
}->{en
}}, $entry->{program_type
};
553 elsif( defined( $chd->{def_pty
} ) and ($chd->{def_pty
} =~ /\S/) )
555 push @
{$d->{category
}->{en
}}, $chd->{def_pty
};
558 if( defined( $entry->{category
} ) and ($entry->{category
} =~ /\S/) )
560 push @
{$d->{category
}->{en
}}, $entry->{category
};
562 elsif( defined( $chd->{def_cat
} ) and ($chd->{def_cat
} =~ /\S/) )
564 push @
{$d->{category
}->{en
}}, $chd->{def_cat
};
567 if( defined( $entry->{production_date
} ) and
568 ($entry->{production_date
} =~ /\S/) )
570 $d->{date
} = substr( $entry->{production_date
}, 0, 4 );
573 if( $entry->{aspect
} ne "unknown" )
575 $d->{video
} = { aspect
=> $entry->{aspect
} };
578 if( $entry->{directors
} =~ /\S/ )
580 $d->{credits
}->{director
} = [split( ", ", $entry->{directors
})];
583 if( $entry->{actors
} =~ /\S/ )
585 $d->{credits
}->{actor
} = [split( ", ", $entry->{actors
})];
588 if( $entry->{writers
} =~ /\S/ )
590 $d->{credits
}->{writer
} = [split( ", ", $entry->{writers
})];
593 if( $entry->{adapters
} =~ /\S/ )
595 $d->{credits
}->{adapter
} = [split( ", ", $entry->{adapters
})];
598 if( $entry->{producers
} =~ /\S/ )
600 $d->{credits
}->{producer
} = [split( ", ", $entry->{producers
})];
603 if( $entry->{presenters
} =~ /\S/ )
605 $d->{credits
}->{presenter
} = [split( ", ", $entry->{presenters
})];
608 if( $entry->{commentators
} =~ /\S/ )
610 $d->{credits
}->{commentator
} = [split( ", ", $entry->{commentators
})];
613 if( $entry->{guests
} =~ /\S/ )
615 $d->{credits
}->{guest
} = [split( ", ", $entry->{guests
})];
622 # Write description of all channels to channels.js.gz.
624 sub ExportChannelList
627 my $ds = $self->{datastore
};
631 my( $res, $sth ) = $ds->sa->Sql( "
632 SELECT * from channels
636 while( my $data = $sth->fetchrow_hashref() )
638 $channels->{$data->{xmltvid
}} = {
640 $self->{Language
} => $data->{display_name
},
642 "baseUrl" => $self->{RootUrl
},
647 $channels->{$data->{xmltvid
}}->{icon
} = $self->{IconRootUrl
} . $data->{xmltvid
} . ".png";
651 my $fh = new IO
::File
("> $self->{Root}channels.js");
652 my $js = JSON
::XS
->new;
655 $fh->print( $js->encode( { jsontv
=> { channels
=> $channels } } ) );
658 system("gzip -f -n $self->{Root}channels.js");
662 # Remove old js-files and js.gz-files.
668 my $ds = $self->{datastore
};
670 # Keep files for the last week.
671 my $keep_date = DateTime
->today->subtract( days
=> 8 )->ymd("-");
673 my @files = glob( $self->{Root
} . "*" );
676 foreach my $file (@files)
679 ($file =~ /(\d\d\d\d-\d\d-\d\d)\.js(\.gz){0,1}/);
681 if( defined( $date ) )
683 # Compare date-strings.
684 if( $date lt $keep_date )
692 p
"Removed $removed files"
698 ### Setup coding system