Added "deletestreamsource" call that deletes a stream AND the corresponding source file.
This commit is contained in:
		
							parent
							
								
									31403f2685
								
							
						
					
					
						commit
						7af419fdad
					
				
					 12 changed files with 146 additions and 10 deletions
				
			
		| 
						 | 
					@ -511,6 +511,81 @@ void Controller::handleAPICommands(JSON::Value & Request, JSON::Value & Response
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  if (Request.isMember("deletestreamsource")){
 | 
				
			||||||
 | 
					    //if array, delete all elements
 | 
				
			||||||
 | 
					    //if object, delete all entries
 | 
				
			||||||
 | 
					    //if string, delete just the one
 | 
				
			||||||
 | 
					    if (Request["deletestreamsource"].isString()){
 | 
				
			||||||
 | 
					      switch (Controller::deleteStream(Request["deletestreamsource"].asStringRef(), Controller::Storage["streams"], true)){
 | 
				
			||||||
 | 
					        case 0:
 | 
				
			||||||
 | 
					          Response["deletestreamsource"] = "0: No action taken";
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 1:
 | 
				
			||||||
 | 
					          Response["deletestreamsource"] = "1: Source file deleted";
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 2:
 | 
				
			||||||
 | 
					          Response["deletestreamsource"] = "2: Source file and dtsh deleted";
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case -1:
 | 
				
			||||||
 | 
					          Response["deletestreamsource"] = "-1: Stream deleted, source remains";
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case -2:
 | 
				
			||||||
 | 
					          Response["deletestreamsource"] = "-2: Stream and source file deleted";
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case -3:
 | 
				
			||||||
 | 
					          Response["deletestreamsource"] = "-3: Stream, source file and dtsh deleted";
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (Request["deletestreamsource"].isArray()){
 | 
				
			||||||
 | 
					      jsonForEach(Request["deletestreamsource"], it){
 | 
				
			||||||
 | 
					        switch (Controller::deleteStream(it->asStringRef(), Controller::Storage["streams"], true)){
 | 
				
			||||||
 | 
					          case 0:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.num()] = "0: No action taken";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case 1:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.num()] = "1: Source file deleted";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case 2:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.num()] = "2: Source file and dtsh deleted";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case -1:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.num()] = "-1: Stream deleted, source remains";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case -2:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.num()] = "-2: Stream and source file deleted";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case -3:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.num()] = "-3: Stream, source file and dtsh deleted";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (Request["deletestreamsource"].isObject()){
 | 
				
			||||||
 | 
					      jsonForEach(Request["deletestreamsource"], it){
 | 
				
			||||||
 | 
					        switch (Controller::deleteStream(it.key(), Controller::Storage["streams"], true)){
 | 
				
			||||||
 | 
					          case 0:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.key()] = "0: No action taken";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case 1:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.key()] = "1: Source file deleted";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case 2:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.key()] = "2: Source file and dtsh deleted";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case -1:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.key()] = "-1: Stream deleted, source remains";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case -2:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.key()] = "-2: Stream and source file deleted";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          case -3:
 | 
				
			||||||
 | 
					            Response["deletestreamsource"][it.key()] = "-3: Stream, source file and dtsh deleted";
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  if (Request.isMember("addprotocol")){
 | 
					  if (Request.isMember("addprotocol")){
 | 
				
			||||||
    if (Request["addprotocol"].isArray()){
 | 
					    if (Request["addprotocol"].isArray()){
 | 
				
			||||||
      jsonForEach(Request["addprotocol"], it){
 | 
					      jsonForEach(Request["addprotocol"], it){
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -293,24 +293,75 @@ namespace Controller {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// \triggers 
 | 
					  /// Deletes the stream (name) from the config (out), optionally also deleting the VoD source file if sourceFileToo is true.
 | 
				
			||||||
  /// The `"STREAM_REMOVE"` trigger is stream-specific, and is ran whenever a stream is removed from the server configuration. If cancelled, the stream is not removed. Its payload is:
 | 
					  int deleteStream(const std::string & name, JSON::Value & out, bool sourceFileToo) {
 | 
				
			||||||
  /// ~~~~~~~~~~~~~~~
 | 
					    int ret = 0;
 | 
				
			||||||
  /// streamname
 | 
					    if (sourceFileToo){
 | 
				
			||||||
  /// ~~~~~~~~~~~~~~~
 | 
					      std::string cleaned = name;
 | 
				
			||||||
  void deleteStream(const std::string & name, JSON::Value & out) {
 | 
					      Util::sanitizeName(cleaned);
 | 
				
			||||||
 | 
					      std::string strmSource;
 | 
				
			||||||
 | 
					      if (Util::getStreamStatus(cleaned) != STRMSTAT_OFF){
 | 
				
			||||||
 | 
					        DTSC::Meta mData = Util::getStreamMeta(cleaned);
 | 
				
			||||||
 | 
					        if (mData.sourceURI.size()){
 | 
				
			||||||
 | 
					          strmSource = mData.sourceURI;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!strmSource.size()){
 | 
				
			||||||
 | 
					        std::string smp = cleaned.substr(0, cleaned.find_first_of("+ "));
 | 
				
			||||||
 | 
					        if (out.isMember(smp) && out[smp].isMember("source")){
 | 
				
			||||||
 | 
					          strmSource = out[smp]["source"].asStringRef();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      bool noFile = false;
 | 
				
			||||||
 | 
					      if (strmSource.size()){
 | 
				
			||||||
 | 
					        std::string prevInput;
 | 
				
			||||||
 | 
					        while (true){
 | 
				
			||||||
 | 
					          std::string oldSrc = strmSource;
 | 
				
			||||||
 | 
					          JSON::Value inputCapa = Util::getInputBySource(oldSrc, true);
 | 
				
			||||||
 | 
					          if (inputCapa["name"].asStringRef() == prevInput){break;}
 | 
				
			||||||
 | 
					          prevInput = inputCapa["name"].asStringRef();
 | 
				
			||||||
 | 
					          strmSource = inputCapa["source_file"].asStringRef();
 | 
				
			||||||
 | 
					          if (!strmSource.size()){
 | 
				
			||||||
 | 
					            noFile = true;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          Util::streamVariables(strmSource, cleaned, oldSrc);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (noFile){
 | 
				
			||||||
 | 
					        WARN_MSG("Not deleting source for stream %s, since the stream does not have an unambiguous source file.", cleaned.c_str());
 | 
				
			||||||
 | 
					      }else{
 | 
				
			||||||
 | 
					        Util::streamVariables(strmSource, cleaned);
 | 
				
			||||||
 | 
					        if (!strmSource.size()){
 | 
				
			||||||
 | 
					          FAIL_MSG("Could not delete source for stream %s: unable to detect stream source URI using any method", cleaned.c_str());
 | 
				
			||||||
 | 
					        }else{
 | 
				
			||||||
 | 
					          if (unlink(strmSource.c_str())){
 | 
				
			||||||
 | 
					            FAIL_MSG("Could not delete source %s for %s: %s (%d)", strmSource.c_str(), cleaned.c_str(), strerror(errno), errno);
 | 
				
			||||||
 | 
					          }else{
 | 
				
			||||||
 | 
					            ++ret;
 | 
				
			||||||
 | 
					            Log("STRM", "Deleting source file for stream "+cleaned+": "+strmSource);
 | 
				
			||||||
 | 
					            //Delete dtsh, ignore failures
 | 
				
			||||||
 | 
					            if (!unlink((strmSource+".dtsh").c_str())){
 | 
				
			||||||
 | 
					              ++ret;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (!out.isMember(name)){
 | 
					    if (!out.isMember(name)){
 | 
				
			||||||
      return;
 | 
					      return ret;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    /*LTS-START*/
 | 
					    /*LTS-START*/
 | 
				
			||||||
    if(Triggers::shouldTrigger("STREAM_REMOVE")){
 | 
					    if(Triggers::shouldTrigger("STREAM_REMOVE")){
 | 
				
			||||||
      if (!Triggers::doTrigger("STREAM_REMOVE", name, name)){
 | 
					      if (!Triggers::doTrigger("STREAM_REMOVE", name, name)){
 | 
				
			||||||
        return;
 | 
					        return ret;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    /*LTS-END*/
 | 
					    /*LTS-END*/
 | 
				
			||||||
    Log("STRM", std::string("Deleted stream ") + name);
 | 
					    Log("STRM", "Deleted stream " + name);
 | 
				
			||||||
    out.removeMember(name);
 | 
					    out.removeMember(name);
 | 
				
			||||||
 | 
					    ++ret;
 | 
				
			||||||
 | 
					    ret *= -1;
 | 
				
			||||||
    if (inputProcesses.count(name)){
 | 
					    if (inputProcesses.count(name)){
 | 
				
			||||||
      pid_t procId = inputProcesses[name];
 | 
					      pid_t procId = inputProcesses[name];
 | 
				
			||||||
      if (Util::Procs::isRunning(procId)){
 | 
					      if (Util::Procs::isRunning(procId)){
 | 
				
			||||||
| 
						 | 
					@ -318,6 +369,7 @@ namespace Controller {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      inputProcesses.erase(name);
 | 
					      inputProcesses.erase(name);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    return ret;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
} //Controller namespace
 | 
					} //Controller namespace
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ namespace Controller {
 | 
				
			||||||
  bool CheckAllStreams(JSON::Value & data);
 | 
					  bool CheckAllStreams(JSON::Value & data);
 | 
				
			||||||
  void CheckStreams(JSON::Value & in, JSON::Value & out);
 | 
					  void CheckStreams(JSON::Value & in, JSON::Value & out);
 | 
				
			||||||
  void AddStreams(JSON::Value & in, JSON::Value & out);
 | 
					  void AddStreams(JSON::Value & in, JSON::Value & out);
 | 
				
			||||||
  void deleteStream(const std::string & name, JSON::Value & out);
 | 
					  int deleteStream(const std::string & name, JSON::Value & out, bool sourceFileToo = false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  struct liveCheck {
 | 
					  struct liveCheck {
 | 
				
			||||||
    long long int lastms;
 | 
					    long long int lastms;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,7 @@ namespace Mist {
 | 
				
			||||||
    capa["name"] = "AV";
 | 
					    capa["name"] = "AV";
 | 
				
			||||||
    capa["desc"] = "This input uses libavformat to read any type of file. Unfortunately this input cannot be redistributed, but it is a great tool for testing the other file-based inputs against.";
 | 
					    capa["desc"] = "This input uses libavformat to read any type of file. Unfortunately this input cannot be redistributed, but it is a great tool for testing the other file-based inputs against.";
 | 
				
			||||||
    capa["source_match"] = "/*";
 | 
					    capa["source_match"] = "/*";
 | 
				
			||||||
 | 
					    capa["source_file"] = "$source";
 | 
				
			||||||
    capa["priority"] = 1ll;
 | 
					    capa["priority"] = 1ll;
 | 
				
			||||||
    capa["codecs"][0u][0u].null();
 | 
					    capa["codecs"][0u][0u].null();
 | 
				
			||||||
    capa["codecs"][0u][1u].null();
 | 
					    capa["codecs"][0u][1u].null();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,7 @@ namespace Mist {
 | 
				
			||||||
    capa["priority"] = 9ll;
 | 
					    capa["priority"] = 9ll;
 | 
				
			||||||
    capa["source_match"].append("/*.dtsc");
 | 
					    capa["source_match"].append("/*.dtsc");
 | 
				
			||||||
    capa["source_match"].append("dtsc://*");
 | 
					    capa["source_match"].append("dtsc://*");
 | 
				
			||||||
 | 
					    capa["source_file"] = "$source";
 | 
				
			||||||
    capa["codecs"][0u][0u].append("H264");
 | 
					    capa["codecs"][0u][0u].append("H264");
 | 
				
			||||||
    capa["codecs"][0u][0u].append("H263");
 | 
					    capa["codecs"][0u][0u].append("H263");
 | 
				
			||||||
    capa["codecs"][0u][0u].append("VP6");
 | 
					    capa["codecs"][0u][0u].append("VP6");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,6 +12,7 @@ namespace Mist{
 | 
				
			||||||
    capa["source_match"].append("/*.mk3d");
 | 
					    capa["source_match"].append("/*.mk3d");
 | 
				
			||||||
    capa["source_match"].append("/*.mks");
 | 
					    capa["source_match"].append("/*.mks");
 | 
				
			||||||
    capa["source_match"].append("/*.webm");
 | 
					    capa["source_match"].append("/*.webm");
 | 
				
			||||||
 | 
					    capa["source_file"] = "$source";
 | 
				
			||||||
    capa["priority"] = 9ll;
 | 
					    capa["priority"] = 9ll;
 | 
				
			||||||
    capa["codecs"].append("H264");
 | 
					    capa["codecs"].append("H264");
 | 
				
			||||||
    capa["codecs"].append("HEVC");
 | 
					    capa["codecs"].append("HEVC");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,7 @@ namespace Mist {
 | 
				
			||||||
    capa["name"] = "FLV";
 | 
					    capa["name"] = "FLV";
 | 
				
			||||||
    capa["desc"] = "Allows loading FLV files for Video on Demand.";
 | 
					    capa["desc"] = "Allows loading FLV files for Video on Demand.";
 | 
				
			||||||
    capa["source_match"] = "/*.flv";
 | 
					    capa["source_match"] = "/*.flv";
 | 
				
			||||||
 | 
					    capa["source_file"] = "$source";
 | 
				
			||||||
    capa["priority"] = 9ll;
 | 
					    capa["priority"] = 9ll;
 | 
				
			||||||
    capa["codecs"][0u][0u].append("H264");
 | 
					    capa["codecs"][0u][0u].append("H264");
 | 
				
			||||||
    capa["codecs"][0u][0u].append("H263");
 | 
					    capa["codecs"][0u][0u].append("H263");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@ namespace Mist {
 | 
				
			||||||
    capa["name"] = "Folder";
 | 
					    capa["name"] = "Folder";
 | 
				
			||||||
    capa["desc"] = "The folder input will make available all supported files in the given folder as streams under this stream name, in the format STREAMNAME+FILENAME. For example, if your stream is called 'files' and you have a file called 'movie.flv', you could access this file streamed as 'files+movie.flv'. This input does not support subdirectories. To support more complex libraries, look into the documentation for the STREAM_SOURCE trigger.";
 | 
					    capa["desc"] = "The folder input will make available all supported files in the given folder as streams under this stream name, in the format STREAMNAME+FILENAME. For example, if your stream is called 'files' and you have a file called 'movie.flv', you could access this file streamed as 'files+movie.flv'. This input does not support subdirectories. To support more complex libraries, look into the documentation for the STREAM_SOURCE trigger.";
 | 
				
			||||||
    capa["source_match"] = "/*/";
 | 
					    capa["source_match"] = "/*/";
 | 
				
			||||||
 | 
					    capa["source_file"] = "$source/$wildcard";
 | 
				
			||||||
    capa["priority"] = 9ll;
 | 
					    capa["priority"] = 9ll;
 | 
				
			||||||
    capa["morphic"] = 1ll;
 | 
					    capa["morphic"] = 1ll;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,6 +17,7 @@ namespace Mist {
 | 
				
			||||||
    capa["name"] = "MP3";
 | 
					    capa["name"] = "MP3";
 | 
				
			||||||
    capa["desc"] = "This input allows you to stream MP3 Video on Demand files.";
 | 
					    capa["desc"] = "This input allows you to stream MP3 Video on Demand files.";
 | 
				
			||||||
    capa["source_match"] = "/*.mp3";
 | 
					    capa["source_match"] = "/*.mp3";
 | 
				
			||||||
 | 
					    capa["source_file"] = "$source";
 | 
				
			||||||
    capa["priority"] = 9ll;
 | 
					    capa["priority"] = 9ll;
 | 
				
			||||||
    capa["codecs"][0u][0u].append("MP3");
 | 
					    capa["codecs"][0u][0u].append("MP3");
 | 
				
			||||||
    timestamp = 0;
 | 
					    timestamp = 0;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -160,6 +160,7 @@ namespace Mist{
 | 
				
			||||||
    capa["name"] = "MP4";
 | 
					    capa["name"] = "MP4";
 | 
				
			||||||
    capa["desc"] = "This input allows streaming of MP4 files as Video on Demand.";
 | 
					    capa["desc"] = "This input allows streaming of MP4 files as Video on Demand.";
 | 
				
			||||||
    capa["source_match"] = "/*.mp4";
 | 
					    capa["source_match"] = "/*.mp4";
 | 
				
			||||||
 | 
					    capa["source_file"] = "$source";
 | 
				
			||||||
    capa["priority"] = 9ll;
 | 
					    capa["priority"] = 9ll;
 | 
				
			||||||
    capa["codecs"][0u][0u].append("HEVC");
 | 
					    capa["codecs"][0u][0u].append("HEVC");
 | 
				
			||||||
    capa["codecs"][0u][0u].append("H264");
 | 
					    capa["codecs"][0u][0u].append("H264");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,6 +48,7 @@ namespace Mist {
 | 
				
			||||||
    capa["name"] = "OGG";
 | 
					    capa["name"] = "OGG";
 | 
				
			||||||
    capa["desc"] = "This input allows streaming of OGG files as Video on Demand.";
 | 
					    capa["desc"] = "This input allows streaming of OGG files as Video on Demand.";
 | 
				
			||||||
    capa["source_match"] = "/*.ogg";
 | 
					    capa["source_match"] = "/*.ogg";
 | 
				
			||||||
 | 
					    capa["source_file"] = "$source";
 | 
				
			||||||
    capa["codecs"][0u][0u].append("theora");
 | 
					    capa["codecs"][0u][0u].append("theora");
 | 
				
			||||||
    capa["codecs"][0u][1u].append("vorbis");
 | 
					    capa["codecs"][0u][1u].append("vorbis");
 | 
				
			||||||
    capa["codecs"][0u][1u].append("opus");
 | 
					    capa["codecs"][0u][1u].append("opus");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -113,6 +113,7 @@ namespace Mist {
 | 
				
			||||||
    capa["name"] = "TS";
 | 
					    capa["name"] = "TS";
 | 
				
			||||||
    capa["desc"] = "This input allows you to stream MPEG2-TS data from static files (/*.ts), streamed files or named pipes (stream://*.ts), streamed over HTTP (http://*.ts, http-ts://*), standard input (ts-exec:*), or multicast/unicast UDP sockets (tsudp://*).";
 | 
					    capa["desc"] = "This input allows you to stream MPEG2-TS data from static files (/*.ts), streamed files or named pipes (stream://*.ts), streamed over HTTP (http://*.ts, http-ts://*), standard input (ts-exec:*), or multicast/unicast UDP sockets (tsudp://*).";
 | 
				
			||||||
    capa["source_match"].append("/*.ts");
 | 
					    capa["source_match"].append("/*.ts");
 | 
				
			||||||
 | 
					    capa["source_file"] = "$source";
 | 
				
			||||||
    capa["source_match"].append("stream://*.ts");
 | 
					    capa["source_match"].append("stream://*.ts");
 | 
				
			||||||
    capa["source_match"].append("tsudp://*");
 | 
					    capa["source_match"].append("tsudp://*");
 | 
				
			||||||
    capa["source_match"].append("ts-exec:*");
 | 
					    capa["source_match"].append("ts-exec:*");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue