helpers.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. # Kudos to Werner Robitza, AVEQ GmbH, for helping with ffmpeg
  2. # related content
  3. import hashlib
  4. import json
  5. import math
  6. import os
  7. import random
  8. import shutil
  9. import subprocess
  10. import tempfile
  11. from fractions import Fraction
  12. import filetype
  13. from django.conf import settings
  14. CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  15. CRF_ENCODING_NUM_SECONDS = 2 # 0 * 60 # videos with greater duration will get
  16. # CRF encoding and not two-pass
  17. # Encoding individual chunks may yield quality variations if you use a
  18. # too low bitrate, so if you go for the chunk-based variant
  19. # you should use CRF encoding.
  20. MAX_RATE_MULTIPLIER = 1.5
  21. BUF_SIZE_MULTIPLIER = 1.5
  22. # in seconds, anything between 2 and 6 makes sense
  23. KEYFRAME_DISTANCE = 4
  24. KEYFRAME_DISTANCE_MIN = 2
  25. # speed presets
  26. # see https://trac.ffmpeg.org/wiki/Encode/H.264
  27. X26x_PRESET = "medium" # "medium"
  28. X265_PRESET = "medium"
  29. X26x_PRESET_BIG_HEIGHT = "faster"
  30. # VP9_SPEED = 1 # between 0 and 4, lower is slower
  31. VP9_SPEED = 2
  32. VIDEO_CRFS = {
  33. "h264_baseline": 23,
  34. "h264": 23,
  35. "h265": 28,
  36. "vp9": 32,
  37. }
  38. # video rates for 25 or 60 fps input, for different codecs, in kbps
  39. VIDEO_BITRATES = {
  40. "h264": {
  41. 25: {
  42. 240: 300,
  43. 360: 500,
  44. 480: 1000,
  45. 720: 2500,
  46. 1080: 4500,
  47. 1440: 9000,
  48. 2160: 18000,
  49. },
  50. 60: {720: 3500, 1080: 7500, 1440: 18000, 2160: 40000},
  51. },
  52. "h265": {
  53. 25: {
  54. 240: 150,
  55. 360: 275,
  56. 480: 500,
  57. 720: 1024,
  58. 1080: 1800,
  59. 1440: 4500,
  60. 2160: 10000,
  61. },
  62. 60: {720: 1800, 1080: 3000, 1440: 8000, 2160: 18000},
  63. },
  64. "vp9": {
  65. 25: {
  66. 240: 150,
  67. 360: 275,
  68. 480: 500,
  69. 720: 1024,
  70. 1080: 1800,
  71. 1440: 4500,
  72. 2160: 10000,
  73. },
  74. 60: {720: 1800, 1080: 3000, 1440: 8000, 2160: 18000},
  75. },
  76. }
  77. AUDIO_ENCODERS = {"h264": "aac", "h265": "aac", "vp9": "libopus"}
  78. AUDIO_BITRATES = {"h264": 128, "h265": 128, "vp9": 96}
  79. EXTENSIONS = {"h264": "mp4", "h265": "mp4", "vp9": "webm"}
  80. VIDEO_PROFILES = {"h264": "main", "h265": "main"}
  81. def get_portal_workflow():
  82. return settings.PORTAL_WORKFLOW
  83. def get_default_state(user=None):
  84. # possible states given the portal workflow setting
  85. state = "private"
  86. if settings.PORTAL_WORKFLOW == "public":
  87. state = "public"
  88. if settings.PORTAL_WORKFLOW == "unlisted":
  89. state = "unlisted"
  90. if settings.PORTAL_WORKFLOW == "private_verified":
  91. if user and user.advancedUser:
  92. state = "unlisted"
  93. return state
  94. def get_file_name(filename):
  95. return filename.split("/")[-1]
  96. def get_file_type(filename):
  97. if not os.path.exists(filename):
  98. return None
  99. file_type = None
  100. kind = filetype.guess(filename)
  101. if kind is not None:
  102. if kind.mime.startswith("video"):
  103. file_type = "video"
  104. elif kind.mime.startswith("image"):
  105. file_type = "image"
  106. elif kind.mime.startswith("audio"):
  107. file_type = "audio"
  108. elif "pdf" in kind.mime:
  109. file_type = "pdf"
  110. else:
  111. # TODO: do something for files not supported by filetype lib
  112. pass
  113. return file_type
  114. def rm_file(filename):
  115. if os.path.isfile(filename):
  116. try:
  117. os.remove(filename)
  118. return True
  119. except OSError:
  120. pass
  121. return False
  122. def rm_files(filenames):
  123. if isinstance(filenames, list):
  124. for filename in filenames:
  125. rm_file(filename)
  126. return True
  127. def rm_dir(directory):
  128. if os.path.isdir(directory):
  129. # refuse to delete a dir inside project BASE_DIR
  130. if directory.startswith(settings.BASE_DIR):
  131. try:
  132. shutil.rmtree(directory)
  133. return True
  134. except (FileNotFoundError, PermissionError):
  135. pass
  136. return False
  137. def url_from_path(filename):
  138. # TODO: find a way to preserver http - https ...
  139. return "{0}{1}".format(settings.MEDIA_URL, filename.replace(settings.MEDIA_ROOT, ""))
  140. def create_temp_file(suffix=None, dir=settings.TEMP_DIRECTORY):
  141. tf = tempfile.NamedTemporaryFile(delete=False, suffix=suffix, dir=dir)
  142. return tf.name
  143. def create_temp_dir(suffix=None, dir=settings.TEMP_DIRECTORY):
  144. td = tempfile.mkdtemp(dir=dir)
  145. return td
  146. def produce_friendly_token(token_len=settings.FRIENDLY_TOKEN_LEN):
  147. token = ""
  148. while len(token) != token_len:
  149. token += CHARS[random.randint(0, len(CHARS) - 1)]
  150. return token
  151. def clean_friendly_token(token):
  152. # cleans token
  153. for char in token:
  154. if char not in CHARS:
  155. token.replace(char, "")
  156. return token
  157. def mask_ip(ip_address):
  158. return hashlib.md5(ip_address.encode("utf-8")).hexdigest()
  159. def run_command(cmd, cwd=None):
  160. """
  161. Run a command directly
  162. """
  163. if isinstance(cmd, str):
  164. cmd = cmd.split()
  165. ret = {}
  166. if cwd:
  167. process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
  168. else:
  169. process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  170. stdout, stderr = process.communicate()
  171. # TODO: catch unicodedecodeerrors here...
  172. if process.returncode == 0:
  173. try:
  174. ret["out"] = stdout.decode("utf-8")
  175. except BaseException:
  176. ret["out"] = ""
  177. try:
  178. ret["error"] = stderr.decode("utf-8")
  179. except BaseException:
  180. ret["error"] = ""
  181. else:
  182. try:
  183. ret["error"] = stderr.decode("utf-8")
  184. except BaseException:
  185. ret["error"] = ""
  186. return ret
  187. def media_file_info(input_file):
  188. """
  189. Get the info about an input file, as determined by ffprobe
  190. Returns a dict, with the keys:
  191. - `filename`: Filename
  192. - `file_size`: Size of the file in bytes
  193. - `video_duration`: Duration of the video in `s.msec`
  194. - `video_frame_rate`: Framerate in Hz
  195. - `video_bitrate`: Bitrate of the video stream in kBit/s
  196. - `video_width`: Width in pixels
  197. - `video_height`: Height in pixels
  198. - `video_codec`: Video codec
  199. - `audio_duration`: Duration of the audio in `s.msec`
  200. - `audio_sample_rate`: Audio sample rate in Hz
  201. - `audio_codec`: Audio codec name (`aac`)
  202. - `audio_bitrate`: Bitrate of the video stream in kBit/s
  203. Also returns the video and audio info raw from ffprobe.
  204. """
  205. ret = {}
  206. if not os.path.isfile(input_file):
  207. ret["fail"] = True
  208. return ret
  209. video_info = {}
  210. audio_info = {}
  211. cmd = ["stat", "-c", "%s", input_file]
  212. stdout = run_command(cmd).get("out")
  213. if stdout:
  214. file_size = int(stdout.strip())
  215. else:
  216. ret["fail"] = True
  217. return ret
  218. cmd = ["md5sum", input_file]
  219. stdout = run_command(cmd).get("out")
  220. if stdout:
  221. md5sum = stdout.split()[0]
  222. else:
  223. md5sum = ""
  224. cmd = [
  225. settings.FFPROBE_COMMAND,
  226. "-loglevel",
  227. "error",
  228. "-show_streams",
  229. "-show_entries",
  230. "format=format_name",
  231. "-of",
  232. "json",
  233. input_file,
  234. ]
  235. stdout = run_command(cmd).get("out")
  236. try:
  237. info = json.loads(stdout)
  238. except TypeError:
  239. ret["fail"] = True
  240. return ret
  241. has_video = False
  242. has_audio = False
  243. for stream_info in info["streams"]:
  244. if stream_info["codec_type"] == "video":
  245. video_info = stream_info
  246. has_video = True
  247. if info.get("format") and info["format"].get("format_name", "") in [
  248. "tty",
  249. "image2",
  250. "image2pipe",
  251. "bin",
  252. "png_pipe",
  253. "gif",
  254. ]:
  255. ret["fail"] = True
  256. return ret
  257. elif stream_info["codec_type"] == "audio":
  258. audio_info = stream_info
  259. has_audio = True
  260. if not has_video:
  261. ret["is_video"] = False
  262. ret["is_audio"] = has_audio
  263. ret["audio_info"] = audio_info
  264. return ret
  265. if "duration" in video_info.keys():
  266. video_duration = float(video_info["duration"])
  267. elif "tags" in video_info.keys() and "DURATION" in video_info["tags"]:
  268. duration_str = video_info["tags"]["DURATION"]
  269. try:
  270. hms, msec = duration_str.split(".")
  271. except ValueError:
  272. hms, msec = duration_str.split(",")
  273. total_dur = sum(int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":"))))
  274. video_duration = total_dur + float("0." + msec)
  275. else:
  276. # fallback to format, eg for webm
  277. cmd = [
  278. settings.FFPROBE_COMMAND,
  279. "-loglevel",
  280. "error",
  281. "-show_format",
  282. "-of",
  283. "json",
  284. input_file,
  285. ]
  286. stdout = run_command(cmd).get("out")
  287. format_info = json.loads(stdout)["format"]
  288. try:
  289. video_duration = float(format_info["duration"])
  290. except KeyError:
  291. ret["fail"] = True
  292. return ret
  293. if "bit_rate" in video_info.keys():
  294. video_bitrate = round(float(video_info["bit_rate"]) / 1024.0, 2)
  295. else:
  296. cmd = [
  297. settings.FFPROBE_COMMAND,
  298. "-loglevel",
  299. "error",
  300. "-select_streams",
  301. "v",
  302. "-show_entries",
  303. "packet=size",
  304. "-of",
  305. "compact=p=0:nk=1",
  306. input_file,
  307. ]
  308. stdout = run_command(cmd).get("out")
  309. stream_size = sum([int(line) for line in stdout.split("\n") if line != ""])
  310. video_bitrate = round((stream_size * 8 / 1024.0) / video_duration, 2)
  311. ret = {
  312. "filename": input_file,
  313. "file_size": file_size,
  314. "video_duration": video_duration,
  315. "video_frame_rate": float(Fraction(video_info["r_frame_rate"])),
  316. "video_bitrate": video_bitrate,
  317. "video_width": video_info["width"],
  318. "video_height": video_info["height"],
  319. "video_codec": video_info["codec_name"],
  320. "has_video": has_video,
  321. "has_audio": has_audio,
  322. }
  323. if has_audio:
  324. audio_duration = 1
  325. if "duration" in audio_info.keys():
  326. audio_duration = float(audio_info["duration"])
  327. elif "tags" in audio_info.keys() and "DURATION" in audio_info["tags"]:
  328. duration_str = audio_info["tags"]["DURATION"]
  329. try:
  330. hms, msec = duration_str.split(".")
  331. except ValueError:
  332. hms, msec = duration_str.split(",")
  333. total_dur = sum(int(x) * 60 ** i for i, x in enumerate(reversed(hms.split(":"))))
  334. audio_duration = total_dur + float("0." + msec)
  335. else:
  336. # fallback to format, eg for webm
  337. cmd = [
  338. settings.FFPROBE_COMMAND,
  339. "-loglevel",
  340. "error",
  341. "-show_format",
  342. "-of",
  343. "json",
  344. input_file,
  345. ]
  346. stdout = run_command(cmd).get("out")
  347. format_info = json.loads(stdout)["format"]
  348. audio_duration = float(format_info["duration"])
  349. if "bit_rate" in audio_info.keys():
  350. audio_bitrate = round(float(audio_info["bit_rate"]) / 1024.0, 2)
  351. else:
  352. # fall back to calculating from accumulated frame duration
  353. cmd = [
  354. settings.FFPROBE_COMMAND,
  355. "-loglevel",
  356. "error",
  357. "-select_streams",
  358. "a",
  359. "-show_entries",
  360. "packet=size",
  361. "-of",
  362. "compact=p=0:nk=1",
  363. input_file,
  364. ]
  365. stdout = run_command(cmd).get("out")
  366. stream_size = sum([int(line) for line in stdout.split("\n") if line != ""])
  367. audio_bitrate = round((stream_size * 8 / 1024.0) / audio_duration, 2)
  368. ret.update(
  369. {
  370. "audio_duration": audio_duration,
  371. "audio_sample_rate": audio_info["sample_rate"],
  372. "audio_codec": audio_info["codec_name"],
  373. "audio_bitrate": audio_bitrate,
  374. "audio_channels": audio_info["channels"],
  375. }
  376. )
  377. ret["video_info"] = video_info
  378. ret["audio_info"] = audio_info
  379. ret["is_video"] = True
  380. ret["md5sum"] = md5sum
  381. return ret
  382. def calculate_seconds(duration):
  383. # returns seconds, given a ffmpeg extracted string
  384. ret = 0
  385. if isinstance(duration, str):
  386. duration = duration.split(":")
  387. if len(duration) != 3:
  388. return ret
  389. else:
  390. return ret
  391. ret += int(float(duration[2]))
  392. ret += int(float(duration[1])) * 60
  393. ret += int(float(duration[0])) * 60 * 60
  394. return ret
  395. def show_file_size(size):
  396. if size:
  397. size = size / 1000000
  398. size = round(size, 1)
  399. size = "{0}MB".format(str(size))
  400. return size
  401. def get_base_ffmpeg_command(
  402. input_file,
  403. output_file,
  404. has_audio,
  405. codec,
  406. encoder,
  407. audio_encoder,
  408. target_fps,
  409. target_height,
  410. target_rate,
  411. target_rate_audio,
  412. pass_file,
  413. pass_number,
  414. enc_type,
  415. chunk,
  416. ):
  417. """Get the base command for a specific codec, height/rate, and pass
  418. Arguments:
  419. input_file {str} -- input file name
  420. output_file {str} -- output file name
  421. has_audio {bool} -- does the input have audio?
  422. codec {str} -- video codec
  423. encoder {str} -- video encoder
  424. audio_encoder {str} -- audio encoder
  425. target_fps {int} -- target FPS
  426. target_height {int} -- height
  427. target_rate {int} -- target bitrate in kbps
  428. target_rate_audio {int} -- audio target bitrate
  429. pass_file {str} -- path to temp pass file
  430. pass_number {int} -- number of passes
  431. enc_type {str} -- encoding type (twopass or crf)
  432. """
  433. target_fps = int(target_fps)
  434. # avoid Frame rate very high for a muxer not efficiently supporting it.
  435. if target_fps > 90:
  436. target_fps = 90
  437. base_cmd = [
  438. settings.FFMPEG_COMMAND,
  439. "-y",
  440. "-i",
  441. input_file,
  442. "-c:v",
  443. encoder,
  444. "-filter:v",
  445. "scale=-2:" + str(target_height) + ",fps=fps=" + str(target_fps),
  446. # always convert to 4:2:0 -- FIXME: this could be also 4:2:2
  447. # but compatibility will suffer
  448. "-pix_fmt",
  449. "yuv420p",
  450. ]
  451. if enc_type == "twopass":
  452. base_cmd.extend(["-b:v", str(target_rate) + "k"])
  453. elif enc_type == "crf":
  454. base_cmd.extend(["-crf", str(VIDEO_CRFS[codec])])
  455. if encoder == "libvpx-vp9":
  456. base_cmd.extend(["-b:v", str(target_rate) + "k"])
  457. if has_audio:
  458. base_cmd.extend(
  459. [
  460. "-c:a",
  461. audio_encoder,
  462. "-b:a",
  463. str(target_rate_audio) + "k",
  464. # stereo audio only, see https://trac.ffmpeg.org/ticket/5718
  465. "-ac",
  466. "2",
  467. ]
  468. )
  469. # get keyframe distance in frames
  470. keyframe_distance = int(target_fps * KEYFRAME_DISTANCE)
  471. # start building the command
  472. cmd = base_cmd[:]
  473. # preset settings
  474. if encoder == "libvpx-vp9":
  475. if pass_number == 1:
  476. speed = 4
  477. else:
  478. speed = VP9_SPEED
  479. elif encoder in ["libx264"]:
  480. preset = X26x_PRESET
  481. elif encoder in ["libx265"]:
  482. preset = X265_PRESET
  483. if target_height >= 720:
  484. preset = X26x_PRESET_BIG_HEIGHT
  485. if encoder == "libx264":
  486. level = "4.2" if target_height <= 1080 else "5.2"
  487. x264_params = [
  488. "keyint=" + str(keyframe_distance * 2),
  489. "keyint_min=" + str(keyframe_distance),
  490. ]
  491. cmd.extend(
  492. [
  493. "-maxrate",
  494. str(int(int(target_rate) * MAX_RATE_MULTIPLIER)) + "k",
  495. "-bufsize",
  496. str(int(int(target_rate) * BUF_SIZE_MULTIPLIER)) + "k",
  497. "-force_key_frames",
  498. "expr:gte(t,n_forced*" + str(KEYFRAME_DISTANCE) + ")",
  499. "-x264-params",
  500. ":".join(x264_params),
  501. "-preset",
  502. preset,
  503. "-profile:v",
  504. VIDEO_PROFILES[codec],
  505. "-level",
  506. level,
  507. ]
  508. )
  509. if enc_type == "twopass":
  510. cmd.extend(["-passlogfile", pass_file, "-pass", pass_number])
  511. elif encoder == "libx265":
  512. x265_params = [
  513. "vbv-maxrate=" + str(int(int(target_rate) * MAX_RATE_MULTIPLIER)),
  514. "vbv-bufsize=" + str(int(int(target_rate) * BUF_SIZE_MULTIPLIER)),
  515. "keyint=" + str(keyframe_distance * 2),
  516. "keyint_min=" + str(keyframe_distance),
  517. ]
  518. if enc_type == "twopass":
  519. x265_params.extend(["stats=" + str(pass_file), "pass=" + str(pass_number)])
  520. cmd.extend(
  521. [
  522. "-force_key_frames",
  523. "expr:gte(t,n_forced*" + str(KEYFRAME_DISTANCE) + ")",
  524. "-x265-params",
  525. ":".join(x265_params),
  526. "-preset",
  527. preset,
  528. "-profile:v",
  529. VIDEO_PROFILES[codec],
  530. ]
  531. )
  532. elif encoder == "libvpx-vp9":
  533. cmd.extend(
  534. [
  535. "-g",
  536. str(keyframe_distance),
  537. "-keyint_min",
  538. str(keyframe_distance),
  539. "-maxrate",
  540. str(int(int(target_rate) * MAX_RATE_MULTIPLIER)) + "k",
  541. "-bufsize",
  542. str(int(int(target_rate) * BUF_SIZE_MULTIPLIER)) + "k",
  543. "-speed",
  544. speed,
  545. # '-deadline', 'realtime',
  546. ]
  547. )
  548. if enc_type == "twopass":
  549. cmd.extend(["-passlogfile", pass_file, "-pass", pass_number])
  550. cmd.extend(
  551. [
  552. "-strict",
  553. "-2",
  554. ]
  555. )
  556. # end of the command
  557. if pass_number == 1:
  558. cmd.extend(["-an", "-f", "null", "/dev/null"])
  559. elif pass_number == 2:
  560. if output_file.endswith("mp4") and chunk:
  561. cmd.extend(["-movflags", "+faststart"])
  562. cmd.extend([output_file])
  563. return cmd
  564. def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_filename, pass_file, chunk=False):
  565. try:
  566. media_info = json.loads(media_info)
  567. except BaseException:
  568. media_info = {}
  569. if codec == "h264":
  570. encoder = "libx264"
  571. # ext = "mp4"
  572. elif codec in ["h265", "hevc"]:
  573. encoder = "libx265"
  574. # ext = "mp4"
  575. elif codec == "vp9":
  576. encoder = "libvpx-vp9"
  577. # ext = "webm"
  578. else:
  579. return False
  580. src_framerate = media_info.get("video_frame_rate", 30)
  581. if src_framerate <= 30:
  582. target_rate = VIDEO_BITRATES[codec][25].get(resolution)
  583. else:
  584. target_rate = VIDEO_BITRATES[codec][60].get(resolution)
  585. if not target_rate: # INVESTIGATE MORE!
  586. target_rate = VIDEO_BITRATES[codec][25].get(resolution)
  587. if not target_rate:
  588. return False
  589. if media_info.get("video_height") < resolution:
  590. if resolution not in [240, 360]: # always get these two
  591. return False
  592. # if codec == "h264_baseline":
  593. # target_fps = 25
  594. # else:
  595. # adjust the target frame rate if the input is fractional
  596. target_fps = src_framerate if isinstance(src_framerate, int) else math.ceil(src_framerate)
  597. if media_info.get("video_duration") > CRF_ENCODING_NUM_SECONDS:
  598. enc_type = "crf"
  599. else:
  600. enc_type = "twopass"
  601. if enc_type == "twopass":
  602. passes = [1, 2]
  603. elif enc_type == "crf":
  604. passes = [2]
  605. cmds = []
  606. for pass_number in passes:
  607. cmds.append(
  608. get_base_ffmpeg_command(
  609. media_file,
  610. output_file=output_filename,
  611. has_audio=media_info.get("has_audio"),
  612. codec=codec,
  613. encoder=encoder,
  614. audio_encoder=AUDIO_ENCODERS[codec],
  615. target_fps=target_fps,
  616. target_height=resolution,
  617. target_rate=target_rate,
  618. target_rate_audio=AUDIO_BITRATES[codec],
  619. pass_file=pass_file,
  620. pass_number=pass_number,
  621. enc_type=enc_type,
  622. chunk=chunk,
  623. )
  624. )
  625. return cmds
  626. def clean_query(query):
  627. """This is used to clear text in order to comply with SearchQuery
  628. known exception cases
  629. :param query: str - the query text that we want to clean
  630. :return:
  631. """
  632. if not query:
  633. return ""
  634. chars = ["^", "{", "}", "&", "|", "<", ">", '"', ")", "(", "!", ":", ";", "'", "#"]
  635. for char in chars:
  636. query = query.replace(char, "")
  637. return query.lower()