TVDB: better handling of first run
[nonametv.git] / lib / NonameTV / DataStore / Helper.pm
blob0d3f631e52ae0a5f4393ce1cbd89b8ebe37fc718
1 package NonameTV::DataStore::Helper;
3 use strict;
5 use Carp;
6 use NonameTV::Log qw/d p w f/;
8 =head1 NAME
10 NonameTV::DataStore::Helper
12 =head1 DESCRIPTION
14 Alternative interface to the datastore for NonameTV. Usable for Importers
15 that receive data where each programme entry does not contain a stop-time
16 and a date.
18 The typical calling-sequence is
20 StartBatch
22 StartDate
23 AddProgramme
24 AddProgramme
26 StartDate
27 AddProgramme
28 AddProgramme
30 EndBatch
32 =head1 METHODS
34 =over 4
36 =cut
38 =item new
40 The constructor for the object. Called with a NonameTV::DataStore object
41 and a timezone-string as parameters. If the timezone is omitted,
42 "Europe/Stockholm" is used.
44 After creating the object, you can set DETECT_SEGMENTS:
46 $dsh->{DETECT_SEGMENTS} = 1;
48 This means that the Datastore::Helper will look for programs that seem
49 to belong together, i.e. they have been split into two with another
50 program in between. The algorithm looks for two identical (same title,
51 description and episode) programs with another program between them.
52 If such programs are found, they will be marked with the last part
53 of the episode number as 0/2 and 1/2.
55 =cut
57 sub new
59 my $class = ref( $_[0] ) || $_[0];
61 my $self = { };
62 bless $self, $class;
64 $self->{ds} = $_[1];
65 $self->{timezone} = $_[2] || "Europe/Stockholm";
67 $self->{DETECT_SEGMENTS} = 0;
69 return $self;
72 sub DESTROY
74 my $self = shift;
78 =item StartBatch
80 Called by an importer to signal the start of a batch of updates.
81 Takes two parameters: one containing a string that uniquely identifies
82 a set of programmes (a batch_id) and the channel_id for the channel
83 that this data is for. The channel_id is a numeric index into the
84 channels-table.
86 =cut
88 sub StartBatch
90 my $self = shift;
91 my( $batch_id, $channel_id ) = @_;
93 $self->{batch_id} = $batch_id;
94 $self->{channel_id} = $channel_id;
96 $self->{lasttime} = undef;
97 $self->{save_ce} = undef;
98 $self->{curr_date} = undef;
100 $self->{programs} = [];
101 $self->{ds}->StartBatch( $batch_id );
104 =item EndBatch
106 Called by an importer to signal the end of a batch of updates.
107 Takes two parameters:
109 An integer containing 1 if the batch was processed
110 successfully, 0 if the batch failed and the database should
111 be rolled back to the contents as they were before StartBatch was called.
112 and -1 if the batch should be rolled back because it has not changed.
114 A string containing a log-message to add to the batchrecord. If success==1,
115 then the log-message is stored in the field 'message'. If success==0, then
116 the log-message is stored in abort_message. If success==-1, the log message
117 is not stored. The log-message can be undef.
119 =cut
121 sub EndBatch
123 my( $self, $success, $log ) = @_;
125 if( scalar( @{$self->{programs}} ) > 0 ) {
126 $self->CommitPrograms();
129 $self->{ds}->EndBatch( $success, $log );
132 =item StartDate
134 Signal the start of a new day. Takes two parameters, a date in the
135 format "yyyy-mm-dd" and an optional time in the format "hh:mm".
137 The time is used to signal the earliest possible starttime of the
138 first programme of the day. If the first programme added via
139 AddProgramme after StartDate has a time that is earlier than the time
140 in StartDate, then it is assumed that the programme actually starts a
141 day later. E.g.:
143 $dsh->StartDate( "2008-01-01", "06:00" );
144 $dsh->AddProgramme( {
145 start_time => "01:45",
147 } );
149 The program in the example will get start-time "2008-01-02 01:45".
151 =cut
153 sub StartDate
155 my $self = shift;
156 my( $date, $time ) = @_;
158 if( scalar( @{$self->{programs}} ) > 0 ) {
159 $self->CommitPrograms();
162 d "StartDate: $date";
163 my( $year, $month, $day ) = split( '-', $date );
164 $self->{curr_date} = DateTime->new(
165 year => $year,
166 month => $month,
167 day => $day,
168 hour => 0,
169 minute => 0,
170 second => 0,
171 time_zone => $self->{timezone} );
173 if( $self->{curr_date} < DateTime->today->subtract( days => 31 ) )
175 w "StartDate called with old date, " .
176 $self->{curr_date}->ymd("-") . ".";
178 if( defined( $time ) )
180 $self->{lasttime} = $self->create_dt( $self->{curr_date}, $time );
182 else
184 $self->{lasttime} = undef;
187 $self->{programs} = [];
190 =item AddProgramme
192 Called by an importer to add a programme for the current batch.
193 Takes a single parameter containing a hashref with information
194 about the programme. The hashref does NOT need to contain an end_time.
196 $dsh->AddProgramme( {
197 start_time => "08:00",
198 title => "Morgon-tv",
199 description => "Morgon i TV-soffan",
200 } );
202 If the start_time is less than the end_time of the last programme (or
203 the start_time if no end_time was specified), AddProgramme assumes that
204 the date should be increased by one day.
206 =cut
208 sub AddProgramme
210 my $self = shift;
211 my( $ce ) = @_;
213 # print "AddProgramme: $ce->{start_time} $ce->{title}\n";
215 if( not defined( $self->{curr_date} ) )
217 confess "Helper $self->{batch_id}: You must call StartDate before AddProgramme";
220 my $start_time = $self->create_dt( $self->{curr_date},
221 $ce->{start_time} );
222 if( defined( $self->{lasttime} ) and ($start_time < $self->{lasttime}) )
224 my $new_start_time;
226 # Have to wrap add days => 1 in an eval, since the resulting time
227 # may not exist (due to daylight savings).
228 eval {
229 $new_start_time = $start_time->clone()->add( days => 1 );
232 if( not defined $new_start_time ) {
233 w "Failed to add one day to start_time, adding 24h instead.";
234 $new_start_time = $start_time->clone()->add( hours => 24 );
237 my $dur = $new_start_time - $self->{lasttime};
238 my( $days, $hours ) = $dur->in_units( 'days', 'hours' );
239 $hours += $days*24;
240 if( $hours < 20 )
242 # By adding one day to the start_time, we ended up with a time
243 # that is less than 20 hours after the lasttime. We assume that
244 # this means that adding a day is the right thing to do.
245 $self->{curr_date}->add( days => 1 );
246 $start_time = $new_start_time;
248 else
250 # By adding one day to the start_time, we ended up with a time
251 # that is more than 20 hours after the lasttime. This probably means
252 # that the start_time hasn't wrapped into a new day, but that
253 # there is something wrong with the source-data and the time actually
254 # moves backwards in the schedule.
255 if( not $self->{ds}->{SILENCE_END_START_OVERLAP} )
257 w "Improbable program start " .
258 $start_time->ymd . " " . $start_time->hms . " skipped";
259 return;
263 $ce->{start_time} = $start_time->clone();
265 $self->{lasttime} = $start_time->clone();
267 if( defined( $ce->{end_time} ) )
269 my $stop_time = $self->create_dt( $self->{curr_date},
270 $ce->{end_time} );
271 if( $stop_time < $self->{lasttime} )
273 $stop_time->add( days => 1 );
274 $self->{curr_date}->add( days => 1 );
276 $ce->{end_time} = $stop_time->clone();
277 $self->{lasttime} = $stop_time->clone();
280 $self->AddCE( $ce );
283 sub AddCE
285 my $self = shift;
286 my( $ce ) = @_;
288 $ce->{start_time}->set_time_zone( "UTC" );
289 $ce->{start_time} = $ce->{start_time}->ymd('-') . " " .
290 $ce->{start_time}->hms(':');
292 if( defined( $ce->{end_time} ) )
294 $ce->{end_time}->set_time_zone( "UTC" );
295 $ce->{end_time} = $ce->{end_time}->ymd('-') . " " .
296 $ce->{end_time}->hms(':');
299 $ce->{channel_id} = $self->{channel_id};
301 push @{$self->{programs}}, $ce;
304 sub CommitPrograms {
305 my $self = shift;
307 # Max Programs Between
308 my $MPB = 1;
310 if( $self->{DETECT_SEGMENTS} ) {
311 my $p = $self->{programs};
312 for( my $i=0; $i < scalar(@{$p}) - 2; $i++ ) {
313 for( my $j=$i+2; $j<$i+2+$MPB && $j < scalar( @{$p} ); $j++ ) {
314 if( programs_equal( $p->[$i], $p->[$j] ) ) {
315 # print "Segments found: $p->[$i]->{title}\n";
316 $p->[$i]->{episode} = defined( $p->[$i]->{episode} ) ?
317 $p->[$i]->{episode} . " 0/2" : ". . 0/2";
318 $p->[$j]->{episode} = defined( $p->[$j]->{episode} ) ?
319 $p->[$j]->{episode} . " 1/2" : ". . 1/2";
325 foreach my $ce (@{$self->{programs}}) {
326 $self->{ds}->AddProgramme( $ce );
329 $self->{programs} = [];
332 sub create_dt
334 my $self = shift;
335 my( $date, $time ) = @_;
337 # print $date->ymd('-') . " $time\n";
339 my $dt = $date->clone();
341 my( $hour, $minute, $second ) = split( ":", $time );
343 # Don't die for invalid times during shift to DST.
344 my $res = eval {
345 $dt->set( hour => $hour,
346 minute => $minute,
350 if( not defined $res )
352 w $dt->ymd('-') . " $hour:$minute: $@";
353 $hour++;
354 w "Adjusting to $hour:$minute";
355 $dt->set( hour => $hour,
356 minute => $minute,
360 return $dt;
363 sub str_eq {
364 my( $s1, $s2 ) = @_;
366 return 1 if (not defined($s1)) and (not defined($s2));
367 return 0 if not defined( $s1 );
368 return 0 if not defined( $s2 );
369 return $s1 eq $s2;
372 sub programs_equal {
373 my( $ce1, $ce2 ) = @_;
375 return 0 unless str_eq($ce1->{title}, $ce2->{title});
376 return 0 unless str_eq($ce1->{description}, $ce2->{description});
377 return 0 unless str_eq($ce1->{episode}, $ce2->{episode});
379 return 1;