update supported audio files in settings.py

+ add win_replace_slash function to format win path strings
+ misc
This commit is contained in:
geoffrey45
2023-01-30 15:59:28 +03:00
parent 93a04ba041
commit 7e15680f26
15 changed files with 268 additions and 96 deletions
+38 -7
View File
@@ -2,6 +2,7 @@
Contains all the folder routes.
"""
import os
import psutil
from pathlib import Path
from flask import Blueprint, request
@@ -10,7 +11,7 @@ from app import settings
from app.lib.folderslib import GetFilesAndDirs
from app.db.sqlite.settings import SettingsSQLMethods as db
from app.models import Folder
from app.utils import create_folder_hash
from app.utils import create_folder_hash, is_windows, win_replace_slash
api = Blueprint("folder", __name__, url_prefix="/")
@@ -42,8 +43,8 @@ def get_folder_tree():
return {
"folders": [
Folder(
name=f.name,
path=str(f),
name=f.name if f.name != "" else str(f).replace("\\", "/"),
path=win_replace_slash(str(f)),
has_tracks=True,
is_sym=f.is_symlink(),
path_hash=create_folder_hash(*f.parts[1:]),
@@ -61,26 +62,56 @@ def get_folder_tree():
}
def get_all_drives():
"""
Returns a list of all the drives on a windows machine.
"""
drives = psutil.disk_partitions()
return [d.mountpoint for d in drives]
@api.route("/folder/dir-browser", methods=["POST"])
def list_folders():
"""
Returns a list of all the folders in the given folder.
"""
data = request.get_json()
is_win = is_windows()
try:
req_dir: str = data["folder"]
except KeyError:
req_dir = settings.USER_HOME_DIR
req_dir = "$home"
if req_dir == "$home":
req_dir = settings.USER_HOME_DIR
# req_dir = settings.USER_HOME_DIR
if is_win:
return {
"folders": [
{"name": win_replace_slash(d), "path": win_replace_slash(d)}
for d in get_all_drives()
]
}
entries = os.scandir(req_dir)
req_dir = req_dir + "/"
try:
entries = os.scandir(req_dir)
except PermissionError:
return {"folders": []}
dirs = [e.name for e in entries if e.is_dir() and not e.name.startswith(".")]
dirs = [{"name": d, "path": os.path.join(req_dir, d)} for d in dirs]
dirs = [
{"name": d, "path": win_replace_slash(os.path.join(req_dir, d))} for d in dirs
]
return {
"folders": sorted(dirs, key=lambda i: i["name"]),
}
# todo:
# - handle showing windows disks in root_dir configuration
# - handle the above, but for all partitions mounted in linux.
# - handle the "\" in client's folder page breadcrumb
+25 -17
View File
@@ -48,6 +48,19 @@ def rebuild_store(db_dirs: list[str]):
log.info("Rebuilding library... ✅")
def finalize(new_: list[str], removed_: list[str], db_dirs_: list[str]):
"""
Params:
new_: will be added to the database
removed_: will be removed from the database
db_dirs_: will be used to remove tracks that
are outside these directories from the database and store.
"""
sdb.remove_root_dirs(removed_)
sdb.add_root_dirs(new_)
rebuild_store(db_dirs_)
@api.route("/settings/add-root-dirs", methods=["POST"])
def add_root_dirs():
"""
@@ -66,33 +79,28 @@ def add_root_dirs():
except KeyError:
return msg, 400
def finalize(new_: list[str], removed_: list[str], db_dirs_: list[str]):
sdb.remove_root_dirs(removed_)
sdb.add_root_dirs(new_)
rebuild_store(db_dirs_)
# ---
db_dirs = sdb.get_root_dirs()
_h = "$home"
try:
if db_dirs[0] == _h and new_dirs[0] == _h.strip():
return {"msg": "Not changed!"}
db_home = any([d == _h for d in db_dirs]) # if $home is in db
incoming_home = any([d == _h for d in new_dirs]) # if $home is in incoming
if db_dirs[0] == _h:
sdb.remove_root_dirs(db_dirs)
# handle $home case
if db_home and incoming_home:
return {"msg": "Not changed!"}
if new_dirs[0] == _h:
finalize([_h], db_dirs, [settings.USER_HOME_DIR])
if db_home or incoming_home:
sdb.remove_root_dirs(db_dirs)
return {"root_dirs": [_h]}
except IndexError:
pass
if incoming_home:
finalize([_h], [], [settings.USER_HOME_DIR])
return {"root_dirs": [_h]}
# ---
for _dir in new_dirs:
children = get_child_dirs(_dir, db_dirs)
removed_dirs.extend(children)
# ---
for _dir in removed_dirs:
try:
+3 -2
View File
@@ -1,5 +1,5 @@
import json
from app.db.sqlite.utils import SQLiteManager
from app.utils import win_replace_slash
class SettingsSQLMethods:
@@ -19,7 +19,8 @@ class SettingsSQLMethods:
cur.execute(sql)
dirs = cur.fetchall()
return [dir[0] for dir in dirs]
dirs = [dir[0] for dir in dirs]
return [win_replace_slash(d) for d in dirs]
@staticmethod
def add_root_dirs(dirs: list[str]):
+13 -3
View File
@@ -17,6 +17,7 @@ from app.utils import (
create_folder_hash,
get_all_artists,
remove_duplicates,
win_replace_slash,
)
@@ -174,7 +175,7 @@ class Store:
return Folder(
name=folder.name,
path=str(folder),
path=win_replace_slash(str(folder)),
is_sym=folder.is_symlink(),
has_tracks=True,
path_hash=create_folder_hash(*folder.parts[1:]),
@@ -218,9 +219,18 @@ class Store:
]
all_folders = [Path(f) for f in all_folders]
all_folders = [f for f in all_folders if f.exists()]
# all_folders = [f for f in all_folders if f.exists()]
for path in tqdm(all_folders, desc="Processing folders"):
valid_folders = []
for folder in all_folders:
try:
if folder.exists():
valid_folders.append(folder)
except PermissionError:
pass
for path in tqdm(valid_folders, desc="Processing folders"):
folder = cls.create_folder(str(path))
cls.folders.append(folder)
+19 -6
View File
@@ -4,6 +4,8 @@ from concurrent.futures import ThreadPoolExecutor
from app.db.store import Store
from app.models import Folder, Track
from app.settings import SUPPORTED_FILES
from app.logger import log
from app.utils import win_replace_slash
class GetFilesAndDirs:
@@ -26,14 +28,25 @@ class GetFilesAndDirs:
ext = os.path.splitext(entry.name)[1].lower()
if entry.is_dir() and not entry.name.startswith("."):
dirs.append(entry.path)
dirs.append(win_replace_slash(entry.path))
elif entry.is_file() and ext in SUPPORTED_FILES:
files.append(entry.path)
files.append(win_replace_slash(entry.path))
# sort files by modified time
files.sort(
key=lambda f: os.path.getmtime(f) # pylint: disable=unnecessary-lambda
)
files_ = []
for file in files:
try:
files_.append(
{
"path": file,
"time": os.path.getmtime(file),
}
)
except OSError as e:
log.error(e)
files_.sort(key=lambda f: f["time"])
files = [f["path"] for f in files_]
tracks = Store.get_tracks_by_filepaths(files)
+5 -2
View File
@@ -43,7 +43,10 @@ class Populate:
if len(dirs_to_scan) == 0:
log.warning(
"The root directory is not configured. Open the app in your web browser to configure."
(
"The root directory is not configured. "
+ "Open the app in your webbrowser to configure."
)
)
return
@@ -85,7 +88,7 @@ class Populate:
for file in tqdm(untagged, desc="Reading files"):
if POPULATE_KEY != key:
raise PopulateCancelledError('Populate key changed')
raise PopulateCancelledError("Populate key changed")
tags = get_tags(file)
+31 -9
View File
@@ -1,13 +1,17 @@
import os
import datetime
import os
from io import BytesIO
from tinytag import TinyTag
from PIL import Image, UnidentifiedImageError
from tinytag import TinyTag
from app import settings
from app.utils import create_hash
from app.utils import (
create_hash,
parse_artist_from_filename,
parse_title_from_filename,
win_replace_slash,
)
def parse_album_art(filepath: str):
@@ -81,7 +85,7 @@ def get_tags(filepath: str):
try:
tags = TinyTag.get(filepath)
except: # pylint: disable=bare-except
except: # noqa: E722
return None
no_albumartist: bool = (tags.albumartist == "") or (tags.albumartist is None)
@@ -97,9 +101,22 @@ def get_tags(filepath: str):
for tag in to_filename:
p = getattr(tags, tag)
if p == "" or p is None:
setattr(tags, tag, filename)
maybe = parse_title_from_filename(filename)
setattr(tags, tag, maybe)
to_check = ["album", "artist", "year", "albumartist"]
parse = ["artist", "albumartist"]
for tag in parse:
p = getattr(tags, tag)
if p == "" or p is None:
maybe = parse_artist_from_filename(filename)
if maybe != []:
setattr(tags, tag, ", ".join(maybe))
else:
setattr(tags, tag, "Unknown")
to_check = ["album", "year", "albumartist"]
for prop in to_check:
p = getattr(tags, prop)
if (p is None) or (p == ""):
@@ -127,10 +144,10 @@ def get_tags(filepath: str):
tags.albumhash = create_hash(tags.album, tags.albumartist)
tags.trackhash = create_hash(tags.artist, tags.album, tags.title)
tags.image = f"{tags.albumhash}.webp"
tags.folder = os.path.dirname(filepath)
tags.folder = win_replace_slash(os.path.dirname(filepath))
tags.date = extract_date(tags.year)
tags.filepath = filepath
tags.filepath = win_replace_slash(filepath)
tags.filetype = filetype
tags = tags.__dict__
@@ -157,3 +174,8 @@ def get_tags(filepath: str):
del tags[tag]
return tags
for tag in to_delete:
del tags[tag]
return tags
+7 -3
View File
@@ -63,7 +63,10 @@ class Watcher:
dir_map = [d for d in dir_map if d["realpath"] != d["original"]]
if len(dirs) > 0 and dirs[0] == "$home":
# if len(dirs) > 0 and dirs[0] == "$home":
# dirs = [settings.USER_HOME_DIR]
if any([d == "$home" for d in dirs]):
dirs = [settings.USER_HOME_DIR]
event_handler = Handler(root_dirs=dirs, dir_map=dir_map)
@@ -83,7 +86,7 @@ class Watcher:
try:
self.observer.start()
log.info("Started watchdog")
except FileNotFoundError:
except (FileNotFoundError, PermissionError):
log.error(
"WatchdogError: Failed to start watchdog, root directories could not be resolved."
)
@@ -189,10 +192,11 @@ class Handler(PatternMatchingEventHandler):
def __init__(self, root_dirs: list[str], dir_map: dict[str:str]):
self.root_dirs = root_dirs
self.dir_map = dir_map
patterns = [f"*{f}" for f in settings.SUPPORTED_FILES]
PatternMatchingEventHandler.__init__(
self,
patterns=["*.flac", "*.mp3"],
patterns=patterns,
ignore_directories=True,
case_sensitive=False,
)
+1 -1
View File
@@ -60,7 +60,7 @@ class Track:
if self.artist is not None:
artists = utils.split_artists(self.artist)
featured = utils.extract_featured_artists_from_title(self.title)
featured = utils.parse_feat_from_title(self.title)
original_lower = "-".join([a.lower() for a in artists])
artists.extend([a for a in featured if a.lower() not in original_lower])
+1 -1
View File
@@ -71,7 +71,7 @@ SM_ARTIST_IMG_SIZE = 64
The size of extracted images in pixels
"""
FILES = ["flac", "mp3", "wav", "m4a"]
FILES = ["flac", "mp3", "wav", "m4a", "ogg", "wma", "opus", "alac", "aiff"]
SUPPORTED_FILES = tuple(f".{file}" for file in FILES)
# ===== SQLite =====
+74 -27
View File
@@ -1,19 +1,18 @@
"""
This module contains mini functions for the server.
"""
import random
import re
import string
from pathlib import Path
from datetime import datetime
import hashlib
import os
import platform
import random
import re
import socket as Socket
import hashlib
import string
import threading
import requests
from datetime import datetime
from pathlib import Path
import requests
from unidecode import unidecode
from app import models
@@ -36,7 +35,8 @@ def background(func):
def run_fast_scandir(_dir: str, full=False) -> tuple[list[str], list[str]]:
"""
Scans a directory for files with a specific extension. Returns a list of files and folders in the directory.
Scans a directory for files with a specific extension.
Returns a list of files and folders in the directory.
"""
if _dir == "":
@@ -46,20 +46,20 @@ def run_fast_scandir(_dir: str, full=False) -> tuple[list[str], list[str]]:
files = []
try:
for _files in os.scandir(_dir):
if _files.is_dir() and not _files.name.startswith("."):
subfolders.append(_files.path)
if _files.is_file():
ext = os.path.splitext(_files.name)[1].lower()
for _file in os.scandir(_dir):
if _file.is_dir() and not _file.name.startswith("."):
subfolders.append(_file.path)
if _file.is_file():
ext = os.path.splitext(_file.name)[1].lower()
if ext in SUPPORTED_FILES:
files.append(_files.path)
files.append(win_replace_slash(_file.path))
if full or len(files) == 0:
for _dir in list(subfolders):
sub_dirs, _files = run_fast_scandir(_dir, full=True)
sub_dirs, _file = run_fast_scandir(_dir, full=True)
subfolders.extend(sub_dirs)
files.extend(_files)
except (PermissionError, FileNotFoundError, ValueError):
files.extend(_file)
except (OSError, PermissionError, FileNotFoundError, ValueError):
return [], []
return subfolders, files
@@ -191,7 +191,7 @@ def get_albumartists(albums: list[models.Album]) -> set[str]:
def get_all_artists(
tracks: list[models.Track], albums: list[models.Album]
tracks: list[models.Track], albums: list[models.Album]
) -> list[models.Artist]:
artists_from_tracks = get_artists_from_tracks(tracks)
artist_from_albums = get_albumartists(albums)
@@ -232,7 +232,8 @@ def bisection_search_string(strings: list[str], target: str) -> str | None:
def get_home_res_path(filename: str):
"""
Returns a path to resources in the home directory of this project. Used to resolve resources in builds.
Returns a path to resources in the home directory of this project.
Used to resolve resources in builds.
"""
try:
return (CWD / ".." / filename).resolve()
@@ -259,12 +260,7 @@ def is_windows():
return platform.system() == "Windows"
def split_artists(src: str):
artists = re.split(r"\s*[&,;]\s*", src)
return [a.strip() for a in artists]
def extract_featured_artists_from_title(title: str) -> list[str]:
def parse_feat_from_title(title: str) -> list[str]:
"""
Extracts featured artists from a song title using regex.
"""
@@ -275,7 +271,7 @@ def extract_featured_artists_from_title(title: str) -> list[str]:
return []
artists = match.group(1)
artists = split_artists(artists)
artists = split_artists(artists, with_and=True)
return artists
@@ -284,3 +280,54 @@ def get_random_str(length=5):
Generates a random string of length `length`.
"""
return "".join(random.choices(string.ascii_letters + string.digits, k=length))
def win_replace_slash(path: str):
if is_windows():
return path.replace("\\", "/").replace("//", "/")
return path
def split_artists(src: str, with_and: bool = False):
exp = r"\s*(?:and|&|,|;)\s*" if with_and else r"\s*[,;]\s*"
artists = re.split(exp, src)
return [a.strip() for a in artists]
def parse_artist_from_filename(title: str):
"""
Extracts artist names from a song title using regex.
"""
regex = r"^(.+?)\s*[-–—]\s*(?:.+?)$"
match = re.search(regex, title, re.IGNORECASE)
if not match:
return []
artists = match.group(1)
artists = split_artists(artists)
return artists
def parse_title_from_filename(title: str):
"""
Extracts track title from a song title using regex.
"""
regex = r"^(?:.+?)\s*[-–—]\s*(.+?)$"
match = re.search(regex, title, re.IGNORECASE)
if not match:
return title
res = match.group(1)
# remove text in brackets starting with "official" case insensitive
res = re.sub(r"\s*\([^)]*official[^)]*\)", "", res, flags=re.IGNORECASE)
return res.strip()
# for title in sample_titles:
# print(parse_artist_from_filename(title))
# print(parse_title_from_filename(title))
+1 -1
View File
@@ -18,7 +18,6 @@ from app.utils import background, get_home_res_path, get_ip, is_windows
werkzeug = logging.getLogger("werkzeug")
werkzeug.setLevel(logging.ERROR)
class Variables:
FLASK_PORT = 1970
FLASK_HOST = "localhost"
@@ -180,6 +179,7 @@ if __name__ == "__main__":
log_info()
run_bg_checks()
start_watchdog()
app.run(
debug=True,
threaded=True,
Generated
+34 -7
View File
@@ -340,14 +340,14 @@ tornado = ["tornado (>=0.2)"]
[[package]]
name = "hypothesis"
version = "6.65.0"
version = "6.65.1"
description = "A library for property-based testing"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "hypothesis-6.65.0-py3-none-any.whl", hash = "sha256:24e3219b0b181414c06bb7a62649a6edb471f148d25c9c9687f47505b0f50b1c"},
{file = "hypothesis-6.65.0.tar.gz", hash = "sha256:d25914dd4008b0292d116ac315f01f6691c5460c494a0291c01d96f4bc17fe68"},
{file = "hypothesis-6.65.1-py3-none-any.whl", hash = "sha256:4b7ae16db09151d17e5feebea07f4f84693cc1573c25e280bc92e619df24182b"},
{file = "hypothesis-6.65.1.tar.gz", hash = "sha256:fb9757f4b556fc73c2eaa2c1b7d39d0184c75e4cb77dadaf6fa59373838bd629"},
]
[package.dependencies]
@@ -602,14 +602,14 @@ files = [
[[package]]
name = "pathspec"
version = "0.10.3"
version = "0.11.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"},
{file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"},
{file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"},
{file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
]
[[package]]
@@ -749,6 +749,33 @@ files = [
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "psutil"
version = "5.9.4"
description = "Cross-platform lib for process and system monitoring in Python."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "psutil-5.9.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8"},
{file = "psutil-5.9.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe"},
{file = "psutil-5.9.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549"},
{file = "psutil-5.9.4-cp27-cp27m-win32.whl", hash = "sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad"},
{file = "psutil-5.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94"},
{file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24"},
{file = "psutil-5.9.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7"},
{file = "psutil-5.9.4-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7"},
{file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1"},
{file = "psutil-5.9.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08"},
{file = "psutil-5.9.4-cp36-abi3-win32.whl", hash = "sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff"},
{file = "psutil-5.9.4-cp36-abi3-win_amd64.whl", hash = "sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4"},
{file = "psutil-5.9.4-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e"},
{file = "psutil-5.9.4.tar.gz", hash = "sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62"},
]
[package.extras]
test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
[[package]]
name = "pyinstaller"
version = "5.7.0"
@@ -1261,4 +1288,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.12"
content-hash = "15b3fba920faab237353240b2b3dbb32603744f6a8ff19e77fe6296d5252c2d7"
content-hash = "54e3995dc11627cb8d20d27ba6d593e4d6d102698c9bd3c29d5505287d22b9e2"
+2
View File
@@ -17,6 +17,7 @@ tqdm = "^4.64.0"
rapidfuzz = "^2.13.7"
tinytag = "^1.8.1"
Unidecode = "^1.3.6"
psutil = "^5.9.4"
[tool.poetry.dev-dependencies]
pylint = "^2.15.5"
@@ -28,6 +29,7 @@ pyinstaller = "^5.7.0"
version = "^22.6.0"
allow-prereleases = true
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
+14 -10
View File
@@ -1,33 +1,37 @@
from hypothesis import given
from hypothesis import strategies as st
import app.utils
from hypothesis import given, strategies as st
from app.utils import extract_featured_artists_from_title
from app.utils import parse_feat_from_title
def test_extract_featured_artists_from_title():
test_titles = [
"Own it (Featuring Ed Sheeran & Stormzy)",
"Own it (Featuring Ed Sheeran and Stormzy)",
"Autograph (On my line)(Feat. Lil Peep)(Deluxe)",
"Why so sad? (with Juice Wrld, Lil Peep)",
"Why so sad? (with Juice Wrld/Lil Peep)",
"Simmer (with Burna Boy)",
"Simmer (without Burna Boy)"
"Simmer (without Burna Boy)",
]
results = [
["Ed Sheeran", "Stormzy"],
['Lil Peep'],
["Juice Wrld", "Lil Peep"],
["Ed Sheeran", "Stormzy"],
["Lil Peep"],
["Juice Wrld", "Lil Peep"],
["Juice Wrld/Lil Peep"],
["Burna Boy"],
[]
[],
]
for title, expected in zip(test_titles, results):
assert extract_featured_artists_from_title(title) == expected
assert parse_feat_from_title(title) == expected
# === HYPOTHESIS GHOSTWRITER TESTS ===
@given(__dir=st.text(), full=st.booleans())
def test_fuzz_run_fast_scandir(__dir: str, full) -> None:
app.utils.run_fast_scandir(_dir=__dir, full=full)
# @given(__dir=st.text(), full=st.booleans())
# def test_fuzz_run_fast_scandir(__dir: str, full) -> None:
# app.utils.run_fast_scandir(_dir=__dir, full=full)