mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-05-30 01:15:33 -04:00
When zmc is killed or crashes without writing EndDateTime, three code paths invent a fake end of NOW(), so an event from hours ago appears to extend across the entire down-time. Montage review then paints a bar that makes it look like recorded video exists where it doesn't. Length is flushed to the DB every few seconds during recording, so even crashed events have an accurate last-known duration. Fall back to StartDateTime + Length when EndDateTime IS NULL, and only fall back to NOW() when Length is also 0 (event has no recorded data yet). - web/api/app/Model/Event.php: EndTimeSecs and EndTime virtual fields, which is what the montagereview JS actually reads via the API. - web/ajax/events.php: same fix in the AJAX events list SQL. - web/skins/classic/views/montagereview.php: \$eventsSql kept in sync even though it is no longer executed directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
180 lines
4.7 KiB
PHP
180 lines
4.7 KiB
PHP
<?php
|
|
require_once __DIR__ .'/../../../includes/Event.php';
|
|
|
|
App::uses('AppModel', 'Model');
|
|
/**
|
|
* Event Model
|
|
*
|
|
* @property Monitor $Monitor
|
|
* @property Frame $Frame
|
|
*/
|
|
class Event extends AppModel {
|
|
|
|
/**
|
|
* Use table
|
|
*
|
|
* @var mixed False or table name
|
|
*/
|
|
public $useTable = 'Events';
|
|
|
|
/**
|
|
* Primary key field
|
|
*
|
|
* @var string
|
|
*/
|
|
public $primaryKey = 'Id';
|
|
|
|
/**
|
|
* Display field
|
|
*
|
|
* @var string
|
|
*/
|
|
public $displayField = 'Name';
|
|
|
|
// For events that never wrote EndDateTime (zmc killed/crashed mid-event),
|
|
// fall back to StartDateTime + Length (Length is flushed to the DB every few
|
|
// seconds during recording, so it reflects the actual recorded duration).
|
|
// Only fall back to NOW() if Length is also 0 (event has no recorded data
|
|
// yet, e.g. just started). This prevents montagereview and other consumers
|
|
// from painting an event bar across hours/days of no real recording.
|
|
public $virtualFields = array(
|
|
'StartTimeSecs' => 'UNIX_TIMESTAMP(StartDateTime)',
|
|
'EndTimeSecs' => '(CASE WHEN Event.EndDateTime IS NOT NULL THEN UNIX_TIMESTAMP(Event.EndDateTime) WHEN Event.Length > 0 THEN UNIX_TIMESTAMP(Event.StartDateTime) + Event.Length ELSE UNIX_TIMESTAMP(NOW()) END)',
|
|
'StartTime' => 'StartDateTime',
|
|
'EndTime' => '(CASE WHEN Event.EndDateTime IS NOT NULL THEN Event.EndDateTime WHEN Event.Length > 0 THEN DATE_ADD(Event.StartDateTime, INTERVAL FLOOR(Event.Length) SECOND) ELSE NOW() END)'
|
|
);
|
|
|
|
//The Associations below have been created with all possible keys, those that are not needed can be removed
|
|
|
|
/**
|
|
* belongsTo associations
|
|
*
|
|
* @var array
|
|
*/
|
|
public $belongsTo = array(
|
|
'Monitor' => array(
|
|
'className' => 'Monitor',
|
|
'foreignKey' => 'MonitorId',
|
|
'conditions' => '',
|
|
'fields' => '',
|
|
'order' => ''
|
|
),
|
|
'Storage' => array(
|
|
'className' => 'Storage',
|
|
'joinTable' => 'Storage',
|
|
'foreignKey' => 'StorageId',
|
|
'conditions' => '',
|
|
'fields' => '',
|
|
'order' => ''
|
|
)
|
|
);
|
|
|
|
/**
|
|
* hasMany associations
|
|
*
|
|
* @var array
|
|
*/
|
|
public $hasMany = array(
|
|
'Frame' => array(
|
|
'className' => 'Frame',
|
|
'foreignKey' => 'EventId',
|
|
'dependent' => true,
|
|
'conditions' => '',
|
|
'fields' => '',
|
|
'order' => '',
|
|
'limit' => '',
|
|
'offset' => '',
|
|
'exclusive' => 'true',
|
|
'finderQuery' => '',
|
|
'counterQuery' => ''
|
|
)
|
|
);
|
|
|
|
/**
|
|
* * * hasMany associations
|
|
* * *
|
|
* * * @var array
|
|
* * */
|
|
public $hasAndBelongsToMany = array(
|
|
'Group' => array(
|
|
'className' => 'Group',
|
|
'joinTable' => 'Groups_Monitors',
|
|
'foreignKey' => 'MonitorId',
|
|
'associationForeignKey' => 'MonitorId',
|
|
'unique' => true,
|
|
'dependent' => false,
|
|
'conditions' => '',
|
|
'fields' => '',
|
|
'order' => '',
|
|
'limit' => '',
|
|
'offset' => '',
|
|
'exclusive' => '',
|
|
'finderQuery' => '',
|
|
'counterQuery' => ''
|
|
),
|
|
'Tag' => array(
|
|
'className' => 'Tag',
|
|
'joinTable' => 'Events_Tags',
|
|
'foreignKey' => 'EventId',
|
|
'associationForeignKey' => 'TagId',
|
|
'unique' => true,
|
|
'dependent' => false,
|
|
'conditions' => '',
|
|
'fields' => '',
|
|
'order' => '',
|
|
'limit' => '',
|
|
'offset' => '',
|
|
'exclusive' => '',
|
|
'finderQuery' => '',
|
|
'counterQuery' => ''
|
|
),
|
|
);
|
|
|
|
public $actsAs = array(
|
|
'Containable',
|
|
'CakePHP-Enum-Behavior.Enum' => array(
|
|
'Orientation' => array('ROTATE_0','ROTATE_90','ROTATE_180','ROTATE_270','FLIP_HORI','FLIP_VERT'),
|
|
'Scheme' => array('Deep','Medium','Shallow')
|
|
)
|
|
);
|
|
|
|
public function Relative_Path() {
|
|
$Event = ZM\Event::find_one(['Id'=>$this->id]);
|
|
return $Event ? $Event->Relative_Path() : '';
|
|
} // end function Relative_Path()
|
|
|
|
public function Path() {
|
|
$Event = ZM\Event::find_one(['Id'=>$this->id]);
|
|
return $Event ? $Event->Path() : '';
|
|
}
|
|
|
|
public function Link_Path() {
|
|
$Event = ZM\Event::find_one(['Id'=>$this->id]);
|
|
return $Event ? $Event->Link_Path() : '';
|
|
}
|
|
|
|
public function fileExists($event) {
|
|
if ($event['DefaultVideo']) {
|
|
if (file_exists($this->Path().'/'.$event['DefaultVideo'])) {
|
|
return 1;
|
|
} else {
|
|
ZM\Warning('File does not exist at ' . $this->Path().'/'.$event['DefaultVideo'] );
|
|
ZM\Warning(print_r($this, true));
|
|
}
|
|
} else {
|
|
return 0;
|
|
}
|
|
} // end function fileExists($event)
|
|
|
|
public function fileSize($event) {
|
|
return filesize($this->Path().'/'.$event['DefaultVideo']);
|
|
}
|
|
|
|
public function beforeDelete($cascade=true) {
|
|
$Event = ZM\Event::find_one(['Id'=>$this->id]);
|
|
if ($Event) $Event->delete();
|
|
// Event->delete() will do it all, so cake doesn't have to do anything.
|
|
return false;
|
|
} // end function afterDelete
|
|
}
|