CC-2977: Never delete files from the database
- Files are marked deleted(file_exists to false) on deletion. - Dirs are marked removed(removed flag to true) on removal of watched folder in a usual way. If dir is unmounted, without being removed from watched list first, it will be marked as not exists(exist flag to false) - Manage Media Folders will show if dirs exist or not( mounted or not) - Playlist builder will show if files exists or not
This commit is contained in:
parent
61c5355e8a
commit
04b48d47cc
|
@ -26,6 +26,8 @@ class ApiController extends Zend_Controller_Action
|
|||
->addActionContext('update-liquidsoap-status', 'json')
|
||||
->addActionContext('library-init', 'json')
|
||||
->addActionContext('live-chat', 'json')
|
||||
->addActionContext('update-file-system-mount', 'json')
|
||||
->addActionContext('handle-watched-dir-missing', 'json')
|
||||
->initContext();
|
||||
}
|
||||
|
||||
|
@ -560,16 +562,15 @@ class ApiController extends Zend_Controller_Action
|
|||
|
||||
// update import timestamp
|
||||
Application_Model_Preference::SetImportTimestamp();
|
||||
Logging::log("mode: ".$mode);
|
||||
if ($mode == "create") {
|
||||
$filepath = $md['MDATA_KEY_FILEPATH'];
|
||||
$filepath = str_replace("\\", "", $filepath);
|
||||
$filepath = str_replace("//", "/", $filepath);
|
||||
|
||||
$file = Application_Model_StoredFile::RecallByFilepath($filepath);
|
||||
|
||||
|
||||
if (is_null($file)) {
|
||||
$file = Application_Model_StoredFile::Insert($md);
|
||||
Logging::log("file: ".print_r($file, true));
|
||||
}
|
||||
else {
|
||||
// path already exist
|
||||
|
@ -815,5 +816,78 @@ class ApiController extends Zend_Controller_Action
|
|||
"numEntries"=>Application_Model_Preference::GetLibraryNumEntries()
|
||||
);
|
||||
}
|
||||
|
||||
// handles addition/deletion of mount point which watched dirs reside
|
||||
public function updateFileSystemMountAction(){
|
||||
global $CC_CONFIG;
|
||||
|
||||
$request = $this->getRequest();
|
||||
$api_key = $request->getParam('api_key');
|
||||
if (!in_array($api_key, $CC_CONFIG["apiKey"]))
|
||||
{
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
print 'You are not allowed to access this resource.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$params = $request->getParams();
|
||||
|
||||
$temp_list = $params['mount_list'];
|
||||
$mount_list = explode(',',$temp_list);
|
||||
|
||||
// get all watched dirs
|
||||
$dirs = Application_Model_MusicDir::getWatchedDirs(null, null);
|
||||
|
||||
// dirs to be added to watch list again
|
||||
$addedDirs = array();
|
||||
// dirs to be deleted from watch list
|
||||
$removedDirs = array();
|
||||
|
||||
$tempDirs = Application_Model_MusicDir::getWatchedDirs(true,null);
|
||||
foreach( $tempDirs as $d)
|
||||
{
|
||||
$removedDirs[$d->getDirectory()] = $d;
|
||||
}
|
||||
foreach( $dirs as $dir){
|
||||
// set Exsits as false as default
|
||||
foreach($mount_list as $mount_path){
|
||||
if($mount_path == '/'){
|
||||
continue;
|
||||
}
|
||||
// if dir contaions mount_path
|
||||
if(strstr($dir->getDirectory(),$mount_path)){
|
||||
if($dir->getExistsFlag() == false){
|
||||
$addedDirs[] = $dir;
|
||||
}else{
|
||||
unset($removedDirs[$dir->getDirector()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach($addedDirs as $ad){
|
||||
Application_Model_MusicDir::addWatchedDir($ad->getDirectory(), false);
|
||||
}
|
||||
foreach($removedDirs as $rd){
|
||||
Application_Model_MusicDir::removeWatchedDir($rd->getDirectory(), false);
|
||||
}
|
||||
}
|
||||
|
||||
// handles case where watched dir is missing
|
||||
public function handleWatchedDirMissingAction(){
|
||||
global $CC_CONFIG;
|
||||
|
||||
$request = $this->getRequest();
|
||||
$api_key = $request->getParam('api_key');
|
||||
if (!in_array($api_key, $CC_CONFIG["apiKey"]))
|
||||
{
|
||||
header('HTTP/1.0 401 Unauthorized');
|
||||
print 'You are not allowed to access this resource.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$dir = base64_decode($request->getParam('dir'));
|
||||
Application_Model_MusicDir::removeWatchedDir($dir, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,19 +40,28 @@ class Application_Model_MusicDir {
|
|||
$this->_dir->save();
|
||||
}
|
||||
|
||||
public function setRemoved($flag){
|
||||
public function setExistsFlag($flag){
|
||||
$this->_dir->setExists($flag);
|
||||
$this->_dir->save();
|
||||
}
|
||||
|
||||
public function setRemovedFlag($flag){
|
||||
$this->_dir->setRemoved($flag);
|
||||
$this->_dir->save();
|
||||
}
|
||||
|
||||
public function getRemoved(){
|
||||
public function getRemovedFlag(){
|
||||
return $this->_dir->getRemoved();
|
||||
}
|
||||
|
||||
public function remove()
|
||||
public function getExistsFlag(){
|
||||
return $this->_dir->getExists();
|
||||
}
|
||||
|
||||
public function remove($setRemovedFlag=true)
|
||||
{
|
||||
global $CC_DBC;
|
||||
Logging::log("remove!!");
|
||||
|
||||
$music_dir_id = $this->getId();
|
||||
|
||||
$sql = "SELECT DISTINCT s.instance_id from cc_music_dirs as md LEFT JOIN cc_files as f on f.directory = md.id
|
||||
|
@ -66,24 +75,24 @@ Logging::log("remove!!");
|
|||
|
||||
// set file_exist flag to false
|
||||
foreach( $files as $file_row ){
|
||||
Logging::log(print_r($file_row['id'], true));
|
||||
$temp_file = Application_Model_StoredFile::Recall($file_row['id']);
|
||||
$temp_file->setFileExistFlag(false);
|
||||
if($temp_file != null){
|
||||
$temp_file->setFileExistFlag(false);
|
||||
}
|
||||
}
|
||||
|
||||
Logging::log("remove!!222222");
|
||||
Logging::log(print_r($this->_dir,true));
|
||||
|
||||
// set Removed flat to true
|
||||
self::setRemoved(true);
|
||||
// set RemovedFlag to true
|
||||
if($setRemovedFlag){
|
||||
self::setRemovedFlag(true);
|
||||
}else{
|
||||
self::setExistsFlag(false);
|
||||
}
|
||||
//$res = $this->_dir->delete();
|
||||
|
||||
Logging::log("remove!!44444");
|
||||
foreach ($show_instances as $show_instance_row) {
|
||||
$temp_show = new Application_Model_ShowInstance($show_instance_row["instance_id"]);
|
||||
$temp_show->updateScheduledTime();
|
||||
}
|
||||
Logging::log("remove end!!");
|
||||
Application_Model_RabbitMq::PushSchedule();
|
||||
}
|
||||
|
||||
|
@ -119,7 +128,7 @@ Logging::log("remove end!!");
|
|||
public static function isPathValid($p_path){
|
||||
$dirs = self::getWatchedDirs();
|
||||
$dirs[] = self::getStorDir();
|
||||
Logging::log("dirs: ".print_r($dirs, true));
|
||||
|
||||
foreach ($dirs as $dirObj){
|
||||
$dir = $dirObj->getDirectory();
|
||||
$diff = strlen($dir) - strlen($p_path);
|
||||
|
@ -139,7 +148,7 @@ Logging::log("dirs: ".print_r($dirs, true));
|
|||
}
|
||||
}
|
||||
|
||||
public static function addDir($p_path, $p_type)
|
||||
public static function addDir($p_path, $p_type, $setRemovedFlag=true)
|
||||
{
|
||||
if(!is_dir($p_path)){
|
||||
return array("code"=>2, "error"=>"'$p_path' is not a valid directory.");
|
||||
|
@ -148,14 +157,13 @@ Logging::log("dirs: ".print_r($dirs, true));
|
|||
if($real_path != "/"){
|
||||
$p_path = $real_path;
|
||||
}
|
||||
Logging::log("dir:".print_r($p_path, true));
|
||||
|
||||
$exist_dir = self::getDirByPath($p_path);
|
||||
Logging::log(print_r($exist_dir, true));
|
||||
|
||||
if( $exist_dir == NULL ){
|
||||
Logging::log("new");
|
||||
$dir = new CcMusicDirs();
|
||||
$temp_dir = new CcMusicDirs();
|
||||
$dir = new Application_Model_MusicDir($temp_dir);
|
||||
}else{
|
||||
Logging::log("exist");
|
||||
$dir = $exist_dir;
|
||||
}
|
||||
|
||||
|
@ -167,12 +175,13 @@ Logging::log("dirs: ".print_r($dirs, true));
|
|||
/* isPathValid() checks if path is a substring or a superstring of an
|
||||
* existing dir and if not, throws NestedDirectoryException */
|
||||
self::isPathValid($p_path);
|
||||
$dir->setRemoved(false);
|
||||
$dir->setDirectory($p_path);
|
||||
Logging::log("dir obj:".print_r($dir, true));
|
||||
if( $exist_dir == NULL ){
|
||||
$dir->save();
|
||||
if($setRemovedFlag){
|
||||
$dir->setRemovedFlag(false);
|
||||
}else{
|
||||
$dir->setExistsFlag(true);
|
||||
}
|
||||
$dir->setDirectory($p_path);
|
||||
|
||||
return array("code"=>0);
|
||||
} catch (NestedDirectoryException $nde){
|
||||
$msg = $nde->getMessage();
|
||||
|
@ -183,9 +192,20 @@ Logging::log("dirs: ".print_r($dirs, true));
|
|||
|
||||
}
|
||||
|
||||
public static function addWatchedDir($p_path)
|
||||
/** There are 2 cases where this function can be called.
|
||||
* 1. When watched dir was added
|
||||
* 2. When some dir was watched, but it was unmounted somehow, but gets mounted again
|
||||
*
|
||||
* In case of 1, $setRemovedFlag should be true
|
||||
* In case of 2, $setRemovedFlag should be false
|
||||
*
|
||||
* When $setRemovedFlag is true, it will set "Removed" flag to false
|
||||
* otherwise, it will set "Exists" flag to true
|
||||
**/
|
||||
public static function addWatchedDir($p_path, $setRemovedFlag=true)
|
||||
{
|
||||
$res = self::addDir($p_path, "watched");
|
||||
$res = self::addDir($p_path, "watched", $setRemovedFlag);
|
||||
|
||||
if ($res['code'] == 0){
|
||||
|
||||
//convert "linked" files (Airtime <= 1.8.2) to watched files.
|
||||
|
@ -243,7 +263,6 @@ Logging::log("dirs: ".print_r($dirs, true));
|
|||
|
||||
public static function getDirByPath($p_path)
|
||||
{
|
||||
Logging::log($p_path);
|
||||
$dir = CcMusicDirsQuery::create()
|
||||
->filterByDirectory($p_path)
|
||||
->findOne();
|
||||
|
@ -256,15 +275,26 @@ Logging::log("dirs: ".print_r($dirs, true));
|
|||
return $mus_dir;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getWatchedDirs()
|
||||
|
||||
/**
|
||||
* Search and returns watched dirs
|
||||
*
|
||||
* @param $exists search condition with exists flag
|
||||
* @param $removed search condition with removed flag
|
||||
*/
|
||||
public static function getWatchedDirs($exists=true, $removed=false)
|
||||
{
|
||||
$result = array();
|
||||
|
||||
$dirs = CcMusicDirsQuery::create()
|
||||
->filterByType("watched")
|
||||
->filterByRemoved(false)
|
||||
->find();
|
||||
->filterByType("watched");
|
||||
if($exists !== null){
|
||||
$dirs = $dirs->filterByExists($exists);
|
||||
}
|
||||
if($removed !== null){
|
||||
$dirs = $dirs->filterByRemoved($removed);
|
||||
}
|
||||
$dirs = $dirs->find();
|
||||
|
||||
foreach($dirs as $dir) {
|
||||
$result[] = new Application_Model_MusicDir($dir);
|
||||
|
@ -311,7 +341,7 @@ Logging::log("dirs: ".print_r($dirs, true));
|
|||
{
|
||||
$dirs = CcMusicDirsQuery::create()
|
||||
->filterByType(array("watched", "stor"))
|
||||
->filterByRemoved(false)
|
||||
->filterByExists(true)
|
||||
->find();
|
||||
|
||||
foreach($dirs as $dir) {
|
||||
|
@ -325,17 +355,27 @@ Logging::log("dirs: ".print_r($dirs, true));
|
|||
return null;
|
||||
}
|
||||
|
||||
public static function removeWatchedDir($p_dir){
|
||||
/** There are 2 cases where this function can be called.
|
||||
* 1. When watched dir was removed
|
||||
* 2. When some dir was watched, but it was unmounted
|
||||
*
|
||||
* In case of 1, $setRemovedFlag should be true
|
||||
* In case of 2, $setRemovedFlag should be false
|
||||
*
|
||||
* When $setRemovedFlag is true, it will set "Removed" flag to false
|
||||
* otherwise, it will set "Exists" flag to true
|
||||
**/
|
||||
public static function removeWatchedDir($p_dir, $setRemovedFlag=true){
|
||||
$real_path = realpath($p_dir)."/";
|
||||
if($real_path != "/"){
|
||||
$p_dir = $real_path;
|
||||
}
|
||||
$dir = Application_Model_MusicDir::getDirByPath($p_dir);
|
||||
Logging::log(print_r($dir,true));
|
||||
|
||||
if($dir == NULL){
|
||||
return array("code"=>1,"error"=>"'$p_dir' doesn't exist in the watched list.");
|
||||
}else{
|
||||
$dir->remove();
|
||||
$dir->remove($setRemovedFlag);
|
||||
$data = array();
|
||||
$data["directory"] = $p_dir;
|
||||
Application_Model_RabbitMq::SendMessageToMediaMonitor("remove_watch", $data);
|
||||
|
@ -346,7 +386,7 @@ Logging::log("dirs: ".print_r($dirs, true));
|
|||
public static function splitFilePath($p_filepath)
|
||||
{
|
||||
$mus_dir = self::getWatchedDirFromFilepath($p_filepath);
|
||||
Logging::log("mus_dir:".print_r($mus_dir, true));
|
||||
|
||||
if(is_null($mus_dir)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -354,7 +394,7 @@ Logging::log("mus_dir:".print_r($mus_dir, true));
|
|||
$length_dir = strlen($mus_dir->getDirectory());
|
||||
$fp = substr($p_filepath, $length_dir);
|
||||
|
||||
return array($mus_dir->getDirectory(), $fp);
|
||||
return array($mus_dir->getDirectory(), trim($fp));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -431,7 +431,7 @@ class Application_Model_StoredFile {
|
|||
public function setFilePath($p_filepath)
|
||||
{
|
||||
$path_info = Application_Model_MusicDir::splitFilePath($p_filepath);
|
||||
Logging::log("path_info:".print_r($path_info, true));
|
||||
|
||||
if (is_null($path_info)) {
|
||||
return -1;
|
||||
}
|
||||
|
@ -499,7 +499,9 @@ class Application_Model_StoredFile {
|
|||
$storedFile->_file = $file;
|
||||
|
||||
if(isset($md['MDATA_KEY_FILEPATH'])) {
|
||||
$res = $storedFile->setFilePath($md['MDATA_KEY_FILEPATH']);
|
||||
// removed "//" in the path. Always use '/' for path separator
|
||||
$filepath = str_replace("//", "/", $md['MDATA_KEY_FILEPATH']);
|
||||
$res = $storedFile->setFilePath($filepath);
|
||||
if ($res === -1) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ class CcMusicDirsTableMap extends TableMap {
|
|||
$this->addPrimaryKey('ID', 'Id', 'INTEGER', true, null, null);
|
||||
$this->addColumn('DIRECTORY', 'Directory', 'LONGVARCHAR', false, null, null);
|
||||
$this->addColumn('TYPE', 'Type', 'VARCHAR', false, 255, null);
|
||||
$this->addColumn('EXISTS', 'Exists', 'BOOLEAN', false, null, true);
|
||||
$this->addColumn('REMOVED', 'Removed', 'BOOLEAN', false, null, false);
|
||||
// validators
|
||||
} // initialize()
|
||||
|
|
|
@ -42,6 +42,13 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* The value for the exists field.
|
||||
* Note: this column has a database default value of: true
|
||||
* @var boolean
|
||||
*/
|
||||
protected $exists;
|
||||
|
||||
/**
|
||||
* The value for the removed field.
|
||||
* Note: this column has a database default value of: false
|
||||
|
@ -76,6 +83,7 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
*/
|
||||
public function applyDefaultValues()
|
||||
{
|
||||
$this->exists = true;
|
||||
$this->removed = false;
|
||||
}
|
||||
|
||||
|
@ -119,6 +127,16 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the [exists] column value.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function getExists()
|
||||
{
|
||||
return $this->exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the [removed] column value.
|
||||
*
|
||||
|
@ -189,6 +207,27 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
return $this;
|
||||
} // setType()
|
||||
|
||||
/**
|
||||
* Set the value of [exists] column.
|
||||
*
|
||||
* @param boolean $v new value
|
||||
* @return CcMusicDirs The current object (for fluent API support)
|
||||
*/
|
||||
public function setExists($v)
|
||||
{
|
||||
Logging::log("setting exists to ".print_r($v, true));
|
||||
if ($v !== null) {
|
||||
$v = (boolean) $v;
|
||||
}
|
||||
|
||||
if ($this->exists !== $v || $this->isNew()) {
|
||||
$this->exists = $v;
|
||||
$this->modifiedColumns[] = CcMusicDirsPeer::EXISTS;
|
||||
}
|
||||
|
||||
return $this;
|
||||
} // setExists()
|
||||
|
||||
/**
|
||||
* Set the value of [removed] column.
|
||||
*
|
||||
|
@ -219,6 +258,10 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
*/
|
||||
public function hasOnlyDefaultValues()
|
||||
{
|
||||
if ($this->exists !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->removed !== false) {
|
||||
return false;
|
||||
}
|
||||
|
@ -248,7 +291,8 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
$this->id = ($row[$startcol + 0] !== null) ? (int) $row[$startcol + 0] : null;
|
||||
$this->directory = ($row[$startcol + 1] !== null) ? (string) $row[$startcol + 1] : null;
|
||||
$this->type = ($row[$startcol + 2] !== null) ? (string) $row[$startcol + 2] : null;
|
||||
$this->removed = ($row[$startcol + 3] !== null) ? (boolean) $row[$startcol + 3] : null;
|
||||
$this->exists = ($row[$startcol + 3] !== null) ? (boolean) $row[$startcol + 3] : null;
|
||||
$this->removed = ($row[$startcol + 4] !== null) ? (boolean) $row[$startcol + 4] : null;
|
||||
$this->resetModified();
|
||||
|
||||
$this->setNew(false);
|
||||
|
@ -257,7 +301,7 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
$this->ensureConsistency();
|
||||
}
|
||||
|
||||
return $startcol + 4; // 4 = CcMusicDirsPeer::NUM_COLUMNS - CcMusicDirsPeer::NUM_LAZY_LOAD_COLUMNS).
|
||||
return $startcol + 5; // 5 = CcMusicDirsPeer::NUM_COLUMNS - CcMusicDirsPeer::NUM_LAZY_LOAD_COLUMNS).
|
||||
|
||||
} catch (Exception $e) {
|
||||
throw new PropelException("Error populating CcMusicDirs object", $e);
|
||||
|
@ -584,6 +628,9 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
return $this->getType();
|
||||
break;
|
||||
case 3:
|
||||
return $this->getExists();
|
||||
break;
|
||||
case 4:
|
||||
return $this->getRemoved();
|
||||
break;
|
||||
default:
|
||||
|
@ -612,7 +659,8 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
$keys[0] => $this->getId(),
|
||||
$keys[1] => $this->getDirectory(),
|
||||
$keys[2] => $this->getType(),
|
||||
$keys[3] => $this->getRemoved(),
|
||||
$keys[3] => $this->getExists(),
|
||||
$keys[4] => $this->getRemoved(),
|
||||
);
|
||||
return $result;
|
||||
}
|
||||
|
@ -654,6 +702,9 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
$this->setType($value);
|
||||
break;
|
||||
case 3:
|
||||
$this->setExists($value);
|
||||
break;
|
||||
case 4:
|
||||
$this->setRemoved($value);
|
||||
break;
|
||||
} // switch()
|
||||
|
@ -683,7 +734,8 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
if (array_key_exists($keys[0], $arr)) $this->setId($arr[$keys[0]]);
|
||||
if (array_key_exists($keys[1], $arr)) $this->setDirectory($arr[$keys[1]]);
|
||||
if (array_key_exists($keys[2], $arr)) $this->setType($arr[$keys[2]]);
|
||||
if (array_key_exists($keys[3], $arr)) $this->setRemoved($arr[$keys[3]]);
|
||||
if (array_key_exists($keys[3], $arr)) $this->setExists($arr[$keys[3]]);
|
||||
if (array_key_exists($keys[4], $arr)) $this->setRemoved($arr[$keys[4]]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -698,6 +750,7 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
if ($this->isColumnModified(CcMusicDirsPeer::ID)) $criteria->add(CcMusicDirsPeer::ID, $this->id);
|
||||
if ($this->isColumnModified(CcMusicDirsPeer::DIRECTORY)) $criteria->add(CcMusicDirsPeer::DIRECTORY, $this->directory);
|
||||
if ($this->isColumnModified(CcMusicDirsPeer::TYPE)) $criteria->add(CcMusicDirsPeer::TYPE, $this->type);
|
||||
if ($this->isColumnModified(CcMusicDirsPeer::EXISTS)) $criteria->add(CcMusicDirsPeer::EXISTS, $this->exists);
|
||||
if ($this->isColumnModified(CcMusicDirsPeer::REMOVED)) $criteria->add(CcMusicDirsPeer::REMOVED, $this->removed);
|
||||
|
||||
return $criteria;
|
||||
|
@ -762,6 +815,7 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
{
|
||||
$copyObj->setDirectory($this->directory);
|
||||
$copyObj->setType($this->type);
|
||||
$copyObj->setExists($this->exists);
|
||||
$copyObj->setRemoved($this->removed);
|
||||
|
||||
if ($deepCopy) {
|
||||
|
@ -962,6 +1016,7 @@ abstract class BaseCcMusicDirs extends BaseObject implements Persistent
|
|||
$this->id = null;
|
||||
$this->directory = null;
|
||||
$this->type = null;
|
||||
$this->exists = null;
|
||||
$this->removed = null;
|
||||
$this->alreadyInSave = false;
|
||||
$this->alreadyInValidation = false;
|
||||
|
|
|
@ -26,7 +26,7 @@ abstract class BaseCcMusicDirsPeer {
|
|||
const TM_CLASS = 'CcMusicDirsTableMap';
|
||||
|
||||
/** The total number of columns. */
|
||||
const NUM_COLUMNS = 4;
|
||||
const NUM_COLUMNS = 5;
|
||||
|
||||
/** The number of lazy-loaded columns. */
|
||||
const NUM_LAZY_LOAD_COLUMNS = 0;
|
||||
|
@ -40,6 +40,9 @@ abstract class BaseCcMusicDirsPeer {
|
|||
/** the column name for the TYPE field */
|
||||
const TYPE = 'cc_music_dirs.TYPE';
|
||||
|
||||
/** the column name for the EXISTS field */
|
||||
const EXISTS = 'cc_music_dirs.EXISTS';
|
||||
|
||||
/** the column name for the REMOVED field */
|
||||
const REMOVED = 'cc_music_dirs.REMOVED';
|
||||
|
||||
|
@ -59,12 +62,12 @@ abstract class BaseCcMusicDirsPeer {
|
|||
* e.g. self::$fieldNames[self::TYPE_PHPNAME][0] = 'Id'
|
||||
*/
|
||||
private static $fieldNames = array (
|
||||
BasePeer::TYPE_PHPNAME => array ('Id', 'Directory', 'Type', 'Removed', ),
|
||||
BasePeer::TYPE_STUDLYPHPNAME => array ('id', 'directory', 'type', 'removed', ),
|
||||
BasePeer::TYPE_COLNAME => array (self::ID, self::DIRECTORY, self::TYPE, self::REMOVED, ),
|
||||
BasePeer::TYPE_RAW_COLNAME => array ('ID', 'DIRECTORY', 'TYPE', 'REMOVED', ),
|
||||
BasePeer::TYPE_FIELDNAME => array ('id', 'directory', 'type', 'removed', ),
|
||||
BasePeer::TYPE_NUM => array (0, 1, 2, 3, )
|
||||
BasePeer::TYPE_PHPNAME => array ('Id', 'Directory', 'Type', 'Exists', 'Removed', ),
|
||||
BasePeer::TYPE_STUDLYPHPNAME => array ('id', 'directory', 'type', 'exists', 'removed', ),
|
||||
BasePeer::TYPE_COLNAME => array (self::ID, self::DIRECTORY, self::TYPE, self::EXISTS, self::REMOVED, ),
|
||||
BasePeer::TYPE_RAW_COLNAME => array ('ID', 'DIRECTORY', 'TYPE', 'EXISTS', 'REMOVED', ),
|
||||
BasePeer::TYPE_FIELDNAME => array ('id', 'directory', 'type', 'exists', 'removed', ),
|
||||
BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, )
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -74,12 +77,12 @@ abstract class BaseCcMusicDirsPeer {
|
|||
* e.g. self::$fieldNames[BasePeer::TYPE_PHPNAME]['Id'] = 0
|
||||
*/
|
||||
private static $fieldKeys = array (
|
||||
BasePeer::TYPE_PHPNAME => array ('Id' => 0, 'Directory' => 1, 'Type' => 2, 'Removed' => 3, ),
|
||||
BasePeer::TYPE_STUDLYPHPNAME => array ('id' => 0, 'directory' => 1, 'type' => 2, 'removed' => 3, ),
|
||||
BasePeer::TYPE_COLNAME => array (self::ID => 0, self::DIRECTORY => 1, self::TYPE => 2, self::REMOVED => 3, ),
|
||||
BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'DIRECTORY' => 1, 'TYPE' => 2, 'REMOVED' => 3, ),
|
||||
BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'directory' => 1, 'type' => 2, 'removed' => 3, ),
|
||||
BasePeer::TYPE_NUM => array (0, 1, 2, 3, )
|
||||
BasePeer::TYPE_PHPNAME => array ('Id' => 0, 'Directory' => 1, 'Type' => 2, 'Exists' => 3, 'Removed' => 4, ),
|
||||
BasePeer::TYPE_STUDLYPHPNAME => array ('id' => 0, 'directory' => 1, 'type' => 2, 'exists' => 3, 'removed' => 4, ),
|
||||
BasePeer::TYPE_COLNAME => array (self::ID => 0, self::DIRECTORY => 1, self::TYPE => 2, self::EXISTS => 3, self::REMOVED => 4, ),
|
||||
BasePeer::TYPE_RAW_COLNAME => array ('ID' => 0, 'DIRECTORY' => 1, 'TYPE' => 2, 'EXISTS' => 3, 'REMOVED' => 4, ),
|
||||
BasePeer::TYPE_FIELDNAME => array ('id' => 0, 'directory' => 1, 'type' => 2, 'exists' => 3, 'removed' => 4, ),
|
||||
BasePeer::TYPE_NUM => array (0, 1, 2, 3, 4, )
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -154,11 +157,13 @@ abstract class BaseCcMusicDirsPeer {
|
|||
$criteria->addSelectColumn(CcMusicDirsPeer::ID);
|
||||
$criteria->addSelectColumn(CcMusicDirsPeer::DIRECTORY);
|
||||
$criteria->addSelectColumn(CcMusicDirsPeer::TYPE);
|
||||
$criteria->addSelectColumn(CcMusicDirsPeer::EXISTS);
|
||||
$criteria->addSelectColumn(CcMusicDirsPeer::REMOVED);
|
||||
} else {
|
||||
$criteria->addSelectColumn($alias . '.ID');
|
||||
$criteria->addSelectColumn($alias . '.DIRECTORY');
|
||||
$criteria->addSelectColumn($alias . '.TYPE');
|
||||
$criteria->addSelectColumn($alias . '.EXISTS');
|
||||
$criteria->addSelectColumn($alias . '.REMOVED');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,13 @@
|
|||
* @method CcMusicDirsQuery orderById($order = Criteria::ASC) Order by the id column
|
||||
* @method CcMusicDirsQuery orderByDirectory($order = Criteria::ASC) Order by the directory column
|
||||
* @method CcMusicDirsQuery orderByType($order = Criteria::ASC) Order by the type column
|
||||
* @method CcMusicDirsQuery orderByExists($order = Criteria::ASC) Order by the exists column
|
||||
* @method CcMusicDirsQuery orderByRemoved($order = Criteria::ASC) Order by the removed column
|
||||
*
|
||||
* @method CcMusicDirsQuery groupById() Group by the id column
|
||||
* @method CcMusicDirsQuery groupByDirectory() Group by the directory column
|
||||
* @method CcMusicDirsQuery groupByType() Group by the type column
|
||||
* @method CcMusicDirsQuery groupByExists() Group by the exists column
|
||||
* @method CcMusicDirsQuery groupByRemoved() Group by the removed column
|
||||
*
|
||||
* @method CcMusicDirsQuery leftJoin($relation) Adds a LEFT JOIN clause to the query
|
||||
|
@ -30,11 +32,13 @@
|
|||
* @method CcMusicDirs findOneById(int $id) Return the first CcMusicDirs filtered by the id column
|
||||
* @method CcMusicDirs findOneByDirectory(string $directory) Return the first CcMusicDirs filtered by the directory column
|
||||
* @method CcMusicDirs findOneByType(string $type) Return the first CcMusicDirs filtered by the type column
|
||||
* @method CcMusicDirs findOneByExists(boolean $exists) Return the first CcMusicDirs filtered by the exists column
|
||||
* @method CcMusicDirs findOneByRemoved(boolean $removed) Return the first CcMusicDirs filtered by the removed column
|
||||
*
|
||||
* @method array findById(int $id) Return CcMusicDirs objects filtered by the id column
|
||||
* @method array findByDirectory(string $directory) Return CcMusicDirs objects filtered by the directory column
|
||||
* @method array findByType(string $type) Return CcMusicDirs objects filtered by the type column
|
||||
* @method array findByExists(boolean $exists) Return CcMusicDirs objects filtered by the exists column
|
||||
* @method array findByRemoved(boolean $removed) Return CcMusicDirs objects filtered by the removed column
|
||||
*
|
||||
* @package propel.generator.airtime.om
|
||||
|
@ -206,6 +210,23 @@ abstract class BaseCcMusicDirsQuery extends ModelCriteria
|
|||
return $this->addUsingAlias(CcMusicDirsPeer::TYPE, $type, $comparison);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the query on the exists column
|
||||
*
|
||||
* @param boolean|string $exists The value to use as filter.
|
||||
* Accepts strings ('false', 'off', '-', 'no', 'n', and '0' are false, the rest is true)
|
||||
* @param string $comparison Operator to use for the column comparison, defaults to Criteria::EQUAL
|
||||
*
|
||||
* @return CcMusicDirsQuery The current query, for fluid interface
|
||||
*/
|
||||
public function filterByExists($exists = null, $comparison = null)
|
||||
{
|
||||
if (is_string($exists)) {
|
||||
$exists = in_array(strtolower($exists), array('false', 'off', '-', 'no', 'n', '0')) ? false : true;
|
||||
}
|
||||
return $this->addUsingAlias(CcMusicDirsPeer::EXISTS, $exists, $comparison);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the query on the removed column
|
||||
*
|
||||
|
|
|
@ -33,12 +33,12 @@
|
|||
</ul>
|
||||
<?php endif; ?>
|
||||
</dd>
|
||||
<?php $watched_dirs = Application_Model_MusicDir::getWatchedDirs(); ?>
|
||||
<?php $watched_dirs = Application_Model_MusicDir::getWatchedDirs(null, false); ?>
|
||||
|
||||
<?php if (count($watched_dirs) > 0): ?>
|
||||
<?php foreach($watched_dirs as $watched_dir): ?>
|
||||
<dd class="block-display selected-item">
|
||||
<span><?php echo $watched_dir->getDirectory(); ?></span><span class="ui-icon ui-icon-close"></span>
|
||||
<span><?php echo $watched_dir->getDirectory(); echo " -- Exsits? "; echo ($watched_dir->getExistsFlag())?"YES":"NO";?></span><span class="ui-icon ui-icon-close"></span>
|
||||
</dd>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
|
|
|
@ -17,6 +17,7 @@ if (count($items)) : ?>
|
|||
</div>
|
||||
<div class="text-row">
|
||||
<span class="spl_artist"><?php echo $item["CcFiles"]['artist_name'] ?></span>
|
||||
<span class="spl_artist"><?php echo ($item["CcFiles"]['file_exist'])?"":"NO FILE FOUND!" ?></span>
|
||||
<span class="spl_offset"><?php echo $item["offset"]?></span>
|
||||
</div>
|
||||
<?php //create the crossfade icon.
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
<column name="id" phpName="Id" type="INTEGER" primaryKey="true" autoIncrement="true" required="true"/>
|
||||
<column name="directory" phpName="Directory" type="LONGVARCHAR" required="false"/>
|
||||
<column name="type" phpName="Type" type="VARCHAR" size="255" required="false"/>
|
||||
<column name="exists" phpName="Exists" type="BOOLEAN" required="false" defaultValue="true"/>
|
||||
<column name="removed" phpName="Removed" type="BOOLEAN" required="false" defaultValue="false"/>
|
||||
<unique name="cc_music_dir_unique">
|
||||
<unique-column name="directory"/>
|
||||
|
|
|
@ -42,6 +42,7 @@ CREATE TABLE "cc_music_dirs"
|
|||
"id" serial NOT NULL,
|
||||
"directory" TEXT,
|
||||
"type" VARCHAR(255),
|
||||
"exists" BOOLEAN default 't',
|
||||
"removed" BOOLEAN default 'f',
|
||||
PRIMARY KEY ("id"),
|
||||
CONSTRAINT "cc_music_dir_unique" UNIQUE ("directory")
|
||||
|
|
|
@ -48,6 +48,12 @@ remove_watched_dir = 'remove-watched-dir/format/json/api_key/%%api_key%%/path/%%
|
|||
# URL to tell Airtime we want to add watched directory
|
||||
set_storage_dir = 'set-storage-dir/format/json/api_key/%%api_key%%/path/%%path%%'
|
||||
|
||||
# URL to tell Airtime about file system mount change
|
||||
update_fs_mount = 'update-file-system-mount/format/json/api_key/%%api_key%%'
|
||||
|
||||
# URL to tell Airtime about file system mount change
|
||||
handle_watched_dir_missing = 'handle-watched-dir-missing/format/json/api_key/%%api_key%%/dir/%%dir%%'
|
||||
|
||||
#############################
|
||||
## Config for Recorder
|
||||
#############################
|
||||
|
|
|
@ -20,6 +20,7 @@ import os
|
|||
from urlparse import urlparse
|
||||
import base64
|
||||
from configobj import ConfigObj
|
||||
import string
|
||||
|
||||
AIRTIME_VERSION = "2.0.0"
|
||||
|
||||
|
@ -399,7 +400,7 @@ class AirTimeApiClient(ApiClientInterface):
|
|||
try:
|
||||
url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["media_setup_url"])
|
||||
url = url.replace("%%api_key%%", self.config["api_key"])
|
||||
|
||||
|
||||
response = self.get_response_from_server(url)
|
||||
response = json.loads(response)
|
||||
logger.info("Connected to Airtime Server. Json Media Storage Dir: %s", response)
|
||||
|
@ -582,11 +583,55 @@ class AirTimeApiClient(ApiClientInterface):
|
|||
url = url.replace("%%msg%%", encoded_msg)
|
||||
url = url.replace("%%stream_id%%", stream_id)
|
||||
url = url.replace("%%boot_time%%", time)
|
||||
logger.debug(url)
|
||||
|
||||
req = urllib2.Request(url)
|
||||
response = urllib2.urlopen(req).read()
|
||||
except Exception, e:
|
||||
logger.error("Exception: %s", e)
|
||||
|
||||
"""
|
||||
This function updates status of mounted file system information on airtime
|
||||
"""
|
||||
def update_file_system_mount(self, mount_list):
|
||||
logger = logging.getLogger()
|
||||
try:
|
||||
url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["update_fs_mount"])
|
||||
|
||||
url = url.replace("%%api_key%%", self.config["api_key"])
|
||||
|
||||
data_string = string.join(mount_list, ',')
|
||||
map = [("mount_list", data_string)]
|
||||
data = urllib.urlencode(map)
|
||||
|
||||
req = urllib2.Request(url, data)
|
||||
response = urllib2.urlopen(req).read()
|
||||
logger.info("update file system mount: %s", response)
|
||||
except Exception, e:
|
||||
import traceback
|
||||
top = traceback.format_exc()
|
||||
logger.error('Exception: %s', e)
|
||||
logger.error("traceback: %s", top)
|
||||
|
||||
"""
|
||||
When watched dir is missing(unplugged or something) on boot up, this function will get called
|
||||
and will call approperiate function on Airtime.
|
||||
"""
|
||||
def handle_watched_dir_missing(self, dir):
|
||||
logger = logging.getLogger()
|
||||
try:
|
||||
url = "http://%s:%s/%s/%s" % (self.config["base_url"], str(self.config["base_port"]), self.config["api_base"], self.config["handle_watched_dir_missing"])
|
||||
|
||||
url = url.replace("%%api_key%%", self.config["api_key"])
|
||||
url = url.replace("%%dir%%", base64.b64encode(dir))
|
||||
|
||||
req = urllib2.Request(url)
|
||||
response = urllib2.urlopen(req).read()
|
||||
logger.info("update file system mount: %s", response)
|
||||
except Exception, e:
|
||||
import traceback
|
||||
top = traceback.format_exc()
|
||||
logger.error('Exception: %s', e)
|
||||
logger.error("traceback: %s", top)
|
||||
|
||||
################################################################################
|
||||
# OpenBroadcast API Client
|
||||
|
|
|
@ -62,7 +62,7 @@ try:
|
|||
mmc = MediaMonitorCommon(config, wm=wm)
|
||||
pe = AirtimeProcessEvent(queue=multi_queue, airtime_config=config, wm=wm, mmc=mmc, api_client=api_client)
|
||||
|
||||
bootstrap = AirtimeMediaMonitorBootstrap(logger, pe, api_client, mmc)
|
||||
bootstrap = AirtimeMediaMonitorBootstrap(logger, pe, api_client, mmc, wm)
|
||||
bootstrap.scan()
|
||||
|
||||
notifier = AirtimeNotifier(wm, pe, read_freq=0, timeout=0, airtime_config=config, api_client=api_client, bootstrap=bootstrap, mmc=mmc)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import os
|
||||
import time
|
||||
import pyinotify
|
||||
|
||||
from subprocess import Popen, PIPE
|
||||
from api_clients import api_client
|
||||
|
||||
class AirtimeMediaMonitorBootstrap():
|
||||
|
||||
|
||||
"""AirtimeMediaMonitorBootstrap constructor
|
||||
|
||||
Keyword Arguments:
|
||||
|
@ -13,11 +14,16 @@ class AirtimeMediaMonitorBootstrap():
|
|||
pe -- reference to an instance of ProcessEvent
|
||||
api_clients -- reference of api_clients to communicate with airtime-server
|
||||
"""
|
||||
def __init__(self, logger, pe, api_client, mmc):
|
||||
def __init__(self, logger, pe, api_client, mmc, wm):
|
||||
self.logger = logger
|
||||
self.pe = pe
|
||||
self.api_client = api_client
|
||||
self.mmc = mmc
|
||||
self.wm = wm
|
||||
# add /etc on watch list so we can detect mount
|
||||
self.mount_file = "/etc"
|
||||
self.logger.info("Adding %s on watch list...", self.mount_file)
|
||||
self.wm.add_watch(self.mount_file, pyinotify.ALL_EVENTS, rec=False, auto_add=False)
|
||||
|
||||
"""On bootup we want to scan all directories and look for files that
|
||||
weren't there or files that changed before media-monitor process
|
||||
|
@ -79,6 +85,10 @@ class AirtimeMediaMonitorBootstrap():
|
|||
if len(file_path.strip(" \n")) > 0:
|
||||
all_files_set.add(file_path[len(dir):])
|
||||
|
||||
# if dir doesn't exists, update db
|
||||
if not os.path.exists(dir):
|
||||
self.pe.handle_watched_dir_missing(dir)
|
||||
|
||||
if os.path.exists(self.mmc.timestamp_file):
|
||||
"""find files that have been modified since the last time media-monitor process started."""
|
||||
time_diff_sec = time.time() - os.path.getmtime(self.mmc.timestamp_file)
|
||||
|
|
|
@ -38,6 +38,8 @@ class AirtimeProcessEvent(ProcessEvent):
|
|||
self.mmc = mmc
|
||||
self.api_client = api_client
|
||||
self.create_dict = {}
|
||||
self.mount_file_dir = "/etc";
|
||||
self.mount_file = "/etc/mtab";
|
||||
|
||||
def add_filepath_to_ignore(self, filepath):
|
||||
self.ignore_event.add(filepath)
|
||||
|
@ -74,6 +76,8 @@ class AirtimeProcessEvent(ProcessEvent):
|
|||
|
||||
|
||||
def process_IN_DELETE_SELF(self, event):
|
||||
if event.path in self.mount_file_dir:
|
||||
return
|
||||
self.logger.info("event: %s", event)
|
||||
path = event.path + '/'
|
||||
if event.dir:
|
||||
|
@ -90,6 +94,8 @@ class AirtimeProcessEvent(ProcessEvent):
|
|||
self.logger.info("Removing the watch folder failed: %s", res['msg']['error'])
|
||||
|
||||
def process_IN_CREATE(self, event):
|
||||
if event.path in self.mount_file_dir:
|
||||
return
|
||||
self.logger.info("event: %s", event)
|
||||
if not event.dir:
|
||||
# record the timestamp of the time on IN_CREATE event
|
||||
|
@ -101,6 +107,8 @@ class AirtimeProcessEvent(ProcessEvent):
|
|||
# we used to use IN_CREATE event, but the IN_CREATE event gets fired before the
|
||||
# copy was done. Hence, IN_CLOSE_WRITE is the correct one to handle.
|
||||
def process_IN_CLOSE_WRITE(self, event):
|
||||
if event.path in self.mount_file_dir:
|
||||
return
|
||||
self.logger.info("event: %s", event)
|
||||
self.logger.info("create_dict: %s", self.create_dict)
|
||||
|
||||
|
@ -145,6 +153,9 @@ class AirtimeProcessEvent(ProcessEvent):
|
|||
self.handle_modified_file(event.dir, event.pathname, event.name)
|
||||
|
||||
def handle_modified_file(self, dir, pathname, name):
|
||||
# if /etc/mtab is modified
|
||||
if pathname in self.mount_file:
|
||||
self.handle_mount_change()
|
||||
# update timestamp on create_dict for the entry with pathname as the key
|
||||
if pathname in self.create_dict:
|
||||
self.create_dict[pathname] = time.time()
|
||||
|
@ -152,12 +163,38 @@ class AirtimeProcessEvent(ProcessEvent):
|
|||
self.logger.info("Modified: %s", pathname)
|
||||
if self.mmc.is_audio_file(name):
|
||||
self.file_events.append({'filepath': pathname, 'mode': self.config.MODE_MODIFY})
|
||||
|
||||
|
||||
# if change is detected on /etc/mtab, we check what mount(file system) was added/removed
|
||||
# and act accordingly
|
||||
def handle_mount_change(self):
|
||||
mount_list = [];
|
||||
# parse /etc/mtab
|
||||
fh = open(self.mount_file, 'r')
|
||||
while 1:
|
||||
line = fh.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
line_info = line.split(' ')
|
||||
# the line format is like following:
|
||||
# /dev/sdg1 /media/809D-D2A1 vfat rw,nosuid,nodev,uhelper=udisks..........
|
||||
# so we always get [1] after split to get the mount point
|
||||
mount_list.append(line_info[1])
|
||||
fh.close()
|
||||
self.logger.info("Mount List: %s", mount_list)
|
||||
# send current mount information to Airtime
|
||||
self.api_client.update_file_system_mount(mount_list);
|
||||
|
||||
def handle_watched_dir_missing(self, dir):
|
||||
self.api_client.handle_watched_dir_missing(dir);
|
||||
|
||||
#if a file is moved somewhere, this callback is run. With details about
|
||||
#where the file is being moved from. The corresponding process_IN_MOVED_TO
|
||||
#callback is only called if the destination of the file is also in a watched
|
||||
#directory.
|
||||
def process_IN_MOVED_FROM(self, event):
|
||||
if event.path in self.mount_file:
|
||||
return
|
||||
self.logger.info("process_IN_MOVED_FROM: %s", event)
|
||||
if not event.dir:
|
||||
if event.pathname in self.temp_files:
|
||||
|
@ -171,6 +208,10 @@ class AirtimeProcessEvent(ProcessEvent):
|
|||
|
||||
def process_IN_MOVED_TO(self, event):
|
||||
self.logger.info("process_IN_MOVED_TO: %s", event)
|
||||
# if /etc/mtab is modified
|
||||
filename = self.mount_file_dir +"/mtab"
|
||||
if event.pathname in filename:
|
||||
self.handle_mount_change()
|
||||
#if stuff dropped in stor via a UI move must change file permissions.
|
||||
self.mmc.set_needed_file_permissions(event.pathname, event.dir)
|
||||
if not event.dir:
|
||||
|
@ -221,6 +262,8 @@ class AirtimeProcessEvent(ProcessEvent):
|
|||
|
||||
|
||||
def process_IN_DELETE(self, event):
|
||||
if event.path in self.mount_file_dir:
|
||||
return
|
||||
self.logger.info("process_IN_DELETE: %s", event)
|
||||
self.handle_removed_file(event.dir, event.pathname)
|
||||
|
||||
|
|
Loading…
Reference in New Issue