示例#1
0
class MMVPackageInterface:

    # Hello world!
    def greeter_message(self) -> None:
        debug_prefix = "[MMVPackageInterface.greeter_message]"

        self.terminal_width = shutil.get_terminal_size()[0]

        bias = " " * (math.floor(self.terminal_width / 2) - 14)

        message = \
f"""{debug_prefix} Show greeter message\n{"-"*self.terminal_width}
{bias} __  __   __  __  __     __
{bias}|  \\/  | |  \\/  | \\ \\   / /
{bias}| |\\/| | | |\\/| |  \\ \\ / / 
{bias}| |  | | | |  | |   \\ V /  
{bias}|_|  |_| |_|  |_|    \\_/   
{bias}
{bias} Modular Music Visualizer                      
{bias[:-1]}{(21-len("Version")-len(self.version))*" "}Version {self.version}
{"-"*self.terminal_width}
"""
        logging.info(message)

    def thanks_message(self):
        debug_prefix = "[MMVPackageInterface.thanks_message]"

        # # Print thanks message :)

        self.terminal_width = shutil.get_terminal_size()[0]

        bias = " " * (math.floor(self.terminal_width / 2) - 45)
        message = \
f"""{debug_prefix} Show thanks message
\n{"-"*self.terminal_width}\n
{bias}[+-------------------------------------------------------------------------------------------+]
{bias} |                                                                                           |
{bias} |              :: Thanks for using the Modular Music Visualizer project !! ::               |
{bias} |              ==============================================================               |
{bias} |                                                                                           |
{bias} |  Here's a few official links for MMV:                                                     |
{bias} |                                                                                           |
{bias} |    - Telegram group:          [          https://t.me/modular_music_visualizer         ]  |
{bias} |    - GitHub Repository:       [ https://github.com/Tremeschin/modular-music-visualizer ]  |
{bias} |    - GitLab Repository:       [ https://gitlab.com/Tremeschin/modular-music-visualizer ]  |
{bias} |                                                                                           |
{bias} |  > Always check for the copyright info on the material you are using (audios, images)     |
{bias} |  before distributing the content generated with MMV, I take absolutely no responsibility  |
{bias} |  for any UGC (user generated content) violations. See LICENSE file as well.               |
{bias} |                                                                                           |
{bias} |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  |
{bias} |                                                                                           |
{bias} |             Don't forget sharing your releases made with MMV on the discussion groups :)  |
{bias} |                 Feel free asking for help or giving new ideas for the project as well !!  |
{bias} |                                                                                           |
{bias}[+-------------------------------------------------------------------------------------------+]
\n{"-"*self.terminal_width}
"""
        logging.info(message)

    # MMVSkia works with glfw plus Skia to draw on a GL canvas and pipe
    # through FFmpeg to render a final video. Have Piano Roll options
    # and modules as well!!
    def get_skia_interface(self, **kwargs):
        debug_prefix = "[MMVPackageInterface.get_skia_interface]"

        from mmv.mmvskia import MMVSkiaInterface

        logging.info(
            f"{debug_prefix} Get and return MMVSkiaInterface, kwargs: {kwargs}"
        )

        return MMVSkiaInterface(top_level_interace=self, **kwargs)

    # MMVShader works with GLSL shaders through MPV. Currently most
    # applicable concept is post processing which bumps MMV quality
    # by a lot
    def get_shader_interface(self):
        debug_prefix = "[MMVPackageInterface.get_shader_interface]"
        from mmv.mmvshader import MMVShaderInterface
        logging.info(f"{debug_prefix} Return MMVShaderInterface")
        return MMVShaderInterface(top_level_interace=self)

    # Return one (usually required) setting up encoder
    def get_ffmpeg_wrapper(self):
        debug_prefix = "[MMVPackageInterface.get_ffmpeg_wrapper]"
        from mmv.common.wrappers.wrap_ffmpeg import FFmpegWrapper
        logging.info(f"{debug_prefix} Return FFmpegWrapper")
        return FFmpegWrapper()

    # Return FFplay wrapper, rarely needed but just in case
    def get_ffplay_wrapper(self):
        debug_prefix = "[MMVPackageInterface.get_ffplay_wrapper]"
        from mmv.common.wrappers.wrap_ffplay import FFplayWrapper
        logging.info(f"{debug_prefix} Return FFplayWrapper")
        return FFplayWrapper()

    # Main interface class, mainly sets up root dirs, get config, distributes classes
    # Send platform = "windows", "macos", "linux" for forcing a specific one
    def __init__(self, platform=None, **kwargs) -> None:
        debug_prefix = "[MMVPackageInterface.__init__]"

        # Versioning
        self.version = "2.6"

        # Can only run on Python 64 bits, this expression returns 32 if 32 bit installation
        # and 64 if 64 bit installation, we assert that (assume it's true, quit if it isn't)
        assert (struct.calcsize("P") * 8) == 64, (
            "You don't have an 64 bit Python installation, MMV will not work on 32 bit Python "
            "because skia-python package only distributes 64 bit Python wheels (bundles).\n"
            "This is out of my control, Skia devs don't release 32 bit version of Skia anyways\n\n"
            "See issue [https://github.com/kyamagu/skia-python/issues/21]")

        # # Get this file's path

        sep = os.path.sep

        # Where this file is located, please refer using this on the whole package
        # Refer to it as self.mmv_skia_main.MMV_PACKAGE_ROOT at any depth in the code
        # This deals with the case we used pyinstaller and it'll get the executable path instead
        if getattr(sys, 'frozen', True):
            self.MMV_PACKAGE_ROOT = os.path.dirname(os.path.abspath(__file__))
            print(f"{debug_prefix} Running directly from source code")
            print(
                f"{debug_prefix} Modular Music Visualizer Python package [__init__.py] located at [{self.MMV_PACKAGE_ROOT}]"
            )
        else:
            self.MMV_PACKAGE_ROOT = os.path.dirname(
                os.path.abspath(sys.executable))
            print(f"{debug_prefix} Running from release (sys.executable..?)")
            print(
                f"{debug_prefix} Modular Music Visualizer executable located at [{self.MMV_PACKAGE_ROOT}]"
            )

        # # Load prelude configuration

        print(f"{debug_prefix} Loading prelude configuration file")

        # Build the path the prelude file should be located at
        prelude_file = f"{self.MMV_PACKAGE_ROOT}{sep}prelude.toml"

        print(
            f"{debug_prefix} Attempting to load prelude file located at [{prelude_file}], we cannot continue if this is wrong.."
        )

        # Load the prelude file
        with open(prelude_file, "r") as f:
            self.prelude = toml.loads(f.read())

        print(
            f"{debug_prefix} Loaded prelude configuration file, data: [{self.prelude}]"
        )

        # # # Logging

        # # We can now set up logging as we have where this file is located at

        # # Reset current handlers if any

        print(
            f"{debug_prefix} Resetting Python's logging logger handlers to empty list"
        )

        # Get logger and empty the list
        logger = logging.getLogger()
        logger.handlers = []

        # Handlers on logging to file and shell output, the first one if the user says to
        handlers = [logging.StreamHandler(sys.stdout)]

        # Loglevel is defined in the prelude.toml configuration
        LOG_LEVEL = {
            "critical": logging.CRITICAL,
            "debug": logging.DEBUG,
            "error": logging.ERROR,
            "info": logging.INFO,
            "warn": logging.WARN,
            "notset": logging.NOTSET,
        }.get(self.prelude["logging"]["log_level"])

        # If user chose to log to a file, add its handler..
        if self.prelude["logging"]["log_to_file"]:

            # Hard coded where the log file will be located
            # this is only valid for the last time we run this software
            self.LOG_FILE = f"{self.MMV_PACKAGE_ROOT}{sep}last_log.log"

            # Reset the log file
            with open(self.LOG_FILE, "w") as f:
                print(
                    f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]"
                )
                f.write("")

            # Verbose and append the file handler
            print(
                f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]")
            handlers.append(
                logging.FileHandler(filename=self.LOG_FILE, encoding='utf-8'))

        # .. otherwise just keep the StreamHandler to stdout

        log_format = {
            "informational":
            "[%(levelname)-8s] [%(filename)-32s:%(lineno)-3d] (%(relativeCreated)-6d) %(message)s",
            "pretty":
            "[%(levelname)-8s] (%(relativeCreated)-5d)ms %(message)s",
            "economic":
            "[%(levelname)s::%(filename)s::%(lineno)d] %(message)s",
            "onlymessage": "%(message)s"
        }.get(self.prelude["logging"]["log_format"])

        # Start the logging global class, output to file and stdout
        logging.basicConfig(
            level=LOG_LEVEL,
            format=log_format,
            handlers=handlers,
        )

        # Greeter message :)
        self.greeter_message()

        # Start logging message
        bias = " " * ((self.terminal_width // 2) - 13)
        print(f"{bias[:-1]}# # [ Start Logging ] # #\n")
        print("-" * self.terminal_width + "\n")

        # Log what we'll do next
        logging.info(
            f"{debug_prefix} We're done with the pre configuration of Python's behavior and loading prelude.toml configuration file"
        )

        # Log precise Python version
        sysversion = sys.version.replace("\n", " ").replace("  ", " ")
        logging.info(f"{debug_prefix} Running on Python: [{sysversion}]")

        # # # FIXME: Python 3.9, go home you're drunk

        # Max python version, show info, assert, pretty print
        maximum_working_python_version = (3, 8)
        pversion = sys.version_info

        # Log and check
        logging.info(
            f"{debug_prefix} Checking if Python <= {maximum_working_python_version} for a working version.. "
        )

        # Huh we're on Python 2..?
        if pversion[0] == 2:
            logging.error(
                f"{debug_prefix} Please upgrade to at least Python 3")
            sys.exit(-1)

        # Python is ok
        if (pversion[0] <= maximum_working_python_version[0]) and (
                pversion[1] <= maximum_working_python_version[1]):
            logging.info(f"{debug_prefix} Ok, good python version")
        else:
            # Warn Python 3.9 is a bit unstable, even the developer had issues making it work
            logging.warn(
                f"{debug_prefix} Python 3.9 is acting a bit weird regarding some dependencies on some systems, while it should be possible to run, take it with some grain of salt and report back into the discussions troubles or workarounds you found?"
            )
            input("\n [ Press enter to continue.. ]: ")

        # # The operating system we're on, one of "linux", "windows", "macos"

        # Get the desired name from a dict matching against os.name
        if platform is None:
            self.os = {
                "posix": "linux",
                "nt": "windows",
                "darwin": "macos"
            }.get(os.name)
        else:
            logging.info(
                f"{debug_prefix} Overriding platform OS to = [{platform}]")
            self.os = platform

        # Log which OS we're running
        logging.info(
            f"{debug_prefix} Running Modular Music Visualizer on Operating System: [{self.os}]"
        )
        logging.info(f"{debug_prefix} (os.path.sep) is [{sep}]")

        # # Create interface's classes

        logging.info(f"{debug_prefix} Creating Utils() class")
        self.utils = Utils()

        logging.info(f"{debug_prefix} Creating Download() class")
        self.download = Download()

        # # Common directories between packages

        # Externals
        self.externals_dir = f"{self.MMV_PACKAGE_ROOT}{sep}externals"
        logging.info(f"{debug_prefix} Externals dir is [{self.externals_dir}]")
        self.utils.mkdir_dne(path=self.externals_dir, silent=True)

        # Downloads (inside externals)
        self.downloads_dir = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}downloads"
        logging.info(f"{debug_prefix} Downloads dir is [{self.downloads_dir}]")
        self.utils.mkdir_dne(path=self.downloads_dir, silent=True)

        # Data dir
        self.data_dir = f"{self.MMV_PACKAGE_ROOT}{sep}data"
        logging.info(f"{debug_prefix} Data dir is [{self.data_dir}]")
        self.utils.mkdir_dne(path=self.data_dir, silent=True)

        # Windoe juuuust in case
        if self.os == "windows":
            logging.info(
                f"{debug_prefix} Appending the Externals directory to system path juuuust in case..."
            )
            sys.path.append(self.externals_dir)

        # # Common files

        self.last_session_info_file = f"{self.data_dir}{sep}last_session_info.toml"
        logging.info(
            f"{debug_prefix} Last session info file is [{self.last_session_info_file}], resetting it.."
        )

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)

        # # External dependencies where to append for PATH

        # Externals directory for Linux
        self.externals_dir_linux = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}linux"
        logging.info(
            f"{debug_prefix} Externals directory for Linux OS is [{self.externals_dir_linux}]"
        )
        self.utils.mkdir_dne(path=self.externals_dir_linux, silent=True)

        # Externals directory for Windows
        self.externals_dir_windows = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}windows"
        logging.info(
            f"{debug_prefix} Externals directory for Windows OS is [{self.externals_dir_windows}]"
        )
        self.utils.mkdir_dne(path=self.externals_dir_windows, silent=True)

        # Externals directory for macOS
        self.externals_dir_macos = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}macos"
        logging.info(
            f"{debug_prefix} Externals directory for Darwin OS (macOS) is [{self.externals_dir_macos}]"
        )
        self.utils.mkdir_dne(path=self.externals_dir_macos, silent=True)

        # # This native platform externals dir
        self.externals_dir_this_platform = self.__get_platform_external_dir(
            self.os)
        logging.info(
            f"{debug_prefix} This platform externals directory is: [{self.externals_dir_this_platform}]"
        )

        # Update the externals search path (create one in this case)
        self.update_externals_search_path()

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)

    # Get the target externals dir for this platform
    def __get_platform_external_dir(self, platform):
        debug_prefix = "[MMVPackageInterface.__get_platform_external_dir]"

        # # This platform externals dir
        externals_dir = {
            "linux": self.externals_dir_linux,
            "windows": self.externals_dir_windows,
            "macos": self.externals_dir_macos,
        }.get(platform)

        # log action
        logging.info(
            f"{debug_prefix} Return external dir for platform [{platform}] -> [{externals_dir}]"
        )

        return externals_dir

    # Update the self.EXTERNALS_SEARCH_PATH to every recursive subdirectory on the platform's externals dir
    def update_externals_search_path(self):
        debug_prefix = "[MMVPackageInterface.update_externals_search_path]"

        # The subdirectories on this platform externals folder
        externals_subdirs = self.utils.get_recursively_all_subdirectories(
            self.externals_dir_this_platform)

        # When using some function like Utils.get_executable_with_name, it have an argument
        # called extra_paths, add this for searching for the full externals directory.
        # Preferably use this interface methods like find_binary instead
        self.EXTERNALS_SEARCH_PATH = [self.externals_dir_this_platform]

        # If we do have subdirectories on this platform externals then append to it
        if externals_subdirs:
            self.EXTERNALS_SEARCH_PATH += externals_subdirs

    # Search for something in system's PATH, also searches for the externals folder
    # Don't append the extra .exe because Linux, macOS doesn't have these, returns False if no binary was found
    def find_binary(self, binary):
        debug_prefix = "[MMVPackageInterface.find_binary]"

        logging.info(STEP_SEPARATOR)

        # Append .exe for Windows
        if self.os == "windows":
            binary += ".exe"

        # Log action
        logging.info(
            f"{debug_prefix} Finding binary in PATH and EXTERNALS directories: [{binary}]"
        )

        return self.utils.get_executable_with_name(
            binary, extra_paths=self.EXTERNALS_SEARCH_PATH)

    # Make sure we have some target Externals, downloads latest release for them.
    # For forcing to download the Windows binaries for a release, send platform="windows" for overwriting
    # otherwise it'll be set to this class's os.
    #
    # For FFmpeg, mpv: Linux and macOS people please install from your distro's package manager.
    #
    # Possible values for target are: ["ffmpeg", "mpv", "musescore"]
    #
    def check_download_externals(self, target_externals=[], platform=None):
        debug_prefix = "[MMVPackageInterface.check_download_externals]"

        # Overwrite os if user set to a specific one
        if platform is None:
            platform = self.os
        else:
            # Error assertion, only allow linux, macos or windows target os
            valid = ["linux", "macos", "windows"]
            if not platform in valid:
                err = f"Target os [{platform}] not valid: should be one of {valid}"
                logging.error(f"{debug_prefix} {err}")
                raise RuntimeError(err)

        # Force the externals argument to be a list
        target_externals = self.utils.force_list(target_externals)

        # Log action
        logging.info(
            f"{debug_prefix} Checking externals {target_externals} for os = [{platform}]"
        )

        # We're frozen (running from release..)
        if getattr(sys, 'frozen', False):
            logging.info(
                f"{debug_prefix} Not checking for externals because is executable build.. (should have them bundled?)"
            )
            return

        # Short hand
        sep = os.path.sep

        # The target externals dir for this platform, it must be windows if we're here..
        target_externals_dir = self.__get_platform_external_dir(platform)

        # For each target external
        for external in target_externals:
            debug_prefix = "[MMVPackageInterface.check_download_externals]"
            logging.info(
                f"{debug_prefix} Checking / downloading external: [{external}] for platform [{platform}]"
            )

            # # FFmpeg / FFprobe

            if external == "ffmpeg":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(binary="ffmpeg")
                    continue

                # If we don't have FFmpeg binary on externals dir
                if not self.find_binary("ffmpeg"):

                    # Get the latest release number of ffmpeg
                    repo = "https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest"
                    logging.info(
                        f"{debug_prefix} Getting latest release info on repository: [{repo}]"
                    )
                    ffmpeg_release = json.loads(
                        self.download.get_html_content(repo))

                    # The assets (downloadable stuff)
                    assets = ffmpeg_release["assets"]

                    logging.info(
                        f"{debug_prefix} Available assets to download (checking for non shared, gpl, non vulkan release):"
                    )

                    # Parsing the version we target and want
                    for item in assets:

                        # The name of the
                        name = item["name"]
                        logging.info(f"{debug_prefix} - [{name}]")

                        # Expected stuff
                        is_lgpl = "lgpl" in name
                        is_shared = "shared" in name
                        have_vulkan = "vulkan" in name
                        from_master = "N" in name

                        # Log what we expect
                        logging.info(
                            f"{debug_prefix} - :: Is LGPL:                   [{is_lgpl:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Is Shared:                 [{is_shared:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Have Vulkan:               [{have_vulkan:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Master branch (N in name): [{from_master:<1}] (expect: 0)"
                        )

                        # We have a match!
                        if not (is_lgpl + is_shared + have_vulkan +
                                from_master):
                            logging.info(
                                f"{debug_prefix} - >> :: We have a match!!")
                            download_url = item["browser_download_url"]
                            break

                    logging.info(
                        f"{debug_prefix} Download URL: [{download_url}]")

                    # Where we'll save the compressed zip of FFmpeg
                    ffmpeg_zip = self.downloads_dir + f"{sep}{name}"

                    # Download FFmpeg build
                    self.download.wget(download_url, ffmpeg_zip,
                                       f"FFmpeg v={name}")

                    # Extract the files
                    self.download.extract_zip(ffmpeg_zip, target_externals_dir)

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [ffmpeg] binary in externals / system path!!"
                    )

            # # MPV

            if external == "mpv":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(
                        binary="mpv",
                        help_fix=f"Visit [https://mpv.io/installation/]")
                    continue

                # If we don't have mpv binary on externals dir or system's path
                if not self.find_binary("mpv"):

                    mpv_7z_version = "mpv-x86_64-20201220-git-dde0189.7z"

                    # Where we'll save the compressed zip of FFmpeg
                    mpv_7z = self.downloads_dir + f"{sep}{mpv_7z_version}"

                    # Download mpv build
                    self.download.wget(
                        f"https://sourceforge.net/projects/mpv-player-windows/files/64bit/{mpv_7z_version}/download",
                        mpv_7z, f"MPV v=20201220-git-dde0189")

                    # Where to extract final mpv
                    mpv_extracted_folder = f"{self.externals_dir_this_platform}{sep}" + mpv_7z_version.replace(
                        ".7z", "")
                    self.utils.mkdir_dne(path=mpv_extracted_folder)

                    # Extract the files
                    self.download.extract_file(mpv_7z, mpv_extracted_folder)

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [mpv] binary in externals / system path!!"
                    )

            # # MPV

            if external == "musescore":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(
                        binary="musescore",
                        help_fix=
                        f"Go to [https://musescore.org/en/download] and install for your platform"
                    )
                    continue

                # If we don't have musescore binary on externals dir or system's path
                if not self.find_binary("musescore"):

                    musescore_version = "v3.5.2/MuseScorePortable-3.5.2.311459983-x86.paf.exe"

                    # Download musescore
                    self.download.wget(
                        f"https://cdn.jsdelivr.net/musescore/{musescore_version}",
                        f"{self.externals_dir_this_platform}{sep}musescore.exe",
                        f"Musescore Portable v=[{musescore_version}]")

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [musescore] binary in externals / system path!!"
                    )

            # Update the externals search path because we downloaded stuff
            self.update_externals_search_path()

        logging.info(STEP_SEPARATOR)

    # Ensure we have an external dependency we can't micro manage because too much entropy
    def __cant_micro_manage_external_for_you(self, binary, help_fix=None):
        debug_prefix = "[MMVPackageInterface.__cant_micro_manage_external_for_you]"

        logging.info(
            f"{debug_prefix} You are using Linux or macOS, please make sure you have [{binary}] package binary installed on your distro or on homebrew, we'll just check for it nowm, can't continue if you don't have it.."
        )

        # Can't continue
        if not self.find_binary(binary):
            logging.error(
                f"{debug_prefix} Couldn't find lowercase [{binary}] binary on PATH, install from your Linux distro package manager / macOS homebrew, please install it"
            )

            # Log any extra help we give the user
            if help_fix is not None:
                logging.error(f"{debug_prefix} {help_fix}")

            sys.exit(-1)
示例#2
0
class MMVPackageInterface:

    # Hello world!
    def greeter_message(self) -> None:
        debug_prefix = "[MMVPackageInterface.greeter_message]"

        # Get a bias for printing the message centered
        self.terminal_width = shutil.get_terminal_size()[0]
        bias = " " * (math.floor(self.terminal_width / 2) - 14)

        message = \
f"""{debug_prefix} Show greeter message\n{"-"*self.terminal_width}
{bias} __  __   __  __  __     __
{bias}|  \\/  | |  \\/  | \\ \\   / /
{bias}| |\\/| | | |\\/| |  \\ \\ / / 
{bias}| |  | | | |  | |   \\ V /  
{bias}|_|  |_| |_|  |_|    \\_/   
{bias}
{bias} Modular Music Visualizer                      
{(2 + int( (self.terminal_width/2) - (len("Version") + len(self.version)/2) ))*" "}Version {self.version}
{"-"*self.terminal_width}
"""
        logging.info(message)

    # Thanks message with some official links, warnings
    def thanks_message(self):
        debug_prefix = "[MMVPackageInterface.thanks_message]"

        # Get a bias for printing the message centered
        self.terminal_width = shutil.get_terminal_size()[0]
        bias = " " * (math.floor(self.terminal_width / 2) - 45)

        message = \
f"""{debug_prefix} Show thanks message
\n{"-"*self.terminal_width}\n
{bias}[+-------------------------------------------------------------------------------------------+]
{bias} |                                                                                           |
{bias} |              :: Thanks for using the Modular Music Visualizer project !! ::               |
{bias} |              ==============================================================               |
{bias} |                                                                                           |
{bias} |  Here's a few official links for MMV:                                                     |
{bias} |                                                                                           |
{bias} |    - Telegram group:          [          https://t.me/modular_music_visualizer         ]  |
{bias} |    - GitHub Repository:       [ https://github.com/Tremeschin/modular-music-visualizer ]  |
{bias} |    - GitLab Repository:       [ https://gitlab.com/Tremeschin/modular-music-visualizer ]  |
{bias} |                                                                                           |
{bias} |  > Always check for the copyright info on the material you are using (audios, images)     |
{bias} |  before distributing the content generated with MMV, I take absolutely no responsibility  |
{bias} |  for any UGC (user generated content) violations. See LICENSE file as well.               |
{bias} |                                                                                           |
{bias} |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  |
{bias} |                                                                                           |
{bias} |             Don't forget sharing your releases made with MMV on the discussion groups :)  |
{bias} |                 Feel free asking for help or giving new ideas for the project as well !!  |
{bias} |                                                                                           |
{bias}[+-------------------------------------------------------------------------------------------+]
\n{"-"*self.terminal_width}
"""
        logging.info(message)

    # MMVShader for some post processing or visualization generation
    def ___printshadersmode(self):
        if not hasattr(self, "___printshader"):
            self.___printshader = None
            self.terminal_width = shutil.get_terminal_size()[0]
            bias = " " * (math.floor(self.terminal_width / 2) - 19)
            message = \
f"""Show extension\n{"="*self.terminal_width}
{bias} _____ _               _               
{bias}/  ___| |             | |              
{bias}\\ `--.| |__   __ _  __| | ___ _ __ ___ 
{bias} `--. \\ '_ \\ / _` |/ _` |/ _ \\ '__/ __|
{bias}/\\__/ / | | | (_| | (_| |  __/ |  \\__ \\
{bias}\\____/|_| |_|\\__,_|\\__,_|\\___|_|  |___/
{bias}                            
{bias}
{bias}             + MMV Mode +
{"="*self.terminal_width}
"""
            logging.info(message)

    # Get a moderngl wrapper / interface for rendering fragment shaders, getting their
    # contents, map images, videos and even other shaders into textures
    def get_mmv_shader_mgl(self, **kwargs):
        self.___printshadersmode()
        from mmv.mmvshader.mmv_shader_mgl import MMVShaderMGL
        return MMVShaderMGL

    # Return shader maker interface
    def get_mmv_shader_maker(self, **kwargs):
        self.___printshadersmode()
        from mmv.mmvshader.mmv_shader_maker import MMVShaderMaker
        return MMVShaderMaker

    # Return one (usually required) setting up encoder unless using preview window
    def get_ffmpeg_wrapper(self):
        debug_prefix = "[MMVPackageInterface.get_ffmpeg_wrapper]"
        from mmv.common.wrappers.wrap_ffmpeg import FFmpegWrapper
        logging.info(f"{debug_prefix} Return FFmpegWrapper")
        return FFmpegWrapper(ffmpeg_binary_path=self.find_binary("ffmpeg"))

    # Return FFplay wrapper, rarely needed but just in case
    def get_ffplay_wrapper(self):
        debug_prefix = "[MMVPackageInterface.get_ffplay_wrapper]"
        from mmv.common.wrappers.wrap_ffplay import FFplayWrapper
        logging.info(f"{debug_prefix} Return FFplayWrapper")
        return FFplayWrapper()

    # # Audio sources

    # Real time, reads from a loopback device
    def get_audio_source_realtime(self):
        debug_prefix = "[MMVPackageInterface.get_audio_source_realtime]"
        from mmv.common.cmn_audio import AudioSourceRealtime
        return AudioSourceRealtime()

    # File source, used for headless rendering
    def get_audio_source_file(self):
        debug_prefix = "[MMVPackageInterface.get_audio_source_file]"
        from mmv.common.cmn_audio import AudioSourceFile
        return AudioSourceFile(ffmpeg_wrapper=self.get_ffmpeg_wrapper())

    # Real time, reads from a loopback device
    def get_jumpcutter(self):
        debug_prefix = "[MMVPackageInterface.get_jumpcutter]"
        self.terminal_width = shutil.get_terminal_size()[0]
        bias = " " * (math.floor(self.terminal_width / 2) - 28)
        message = \
f"""{debug_prefix} Show extension\n{"="*self.terminal_width}
{bias}   ___                       _____       _   _            
{bias}  |_  |                     /  __ \\     | | | |           
{bias}    | |_   _ _ __ ___  _ __ | /  \\/_   _| |_| |_ ___ _ __ 
{bias}    | | | | | '_ ` _ \\| '_ \\| |   | | | | __| __/ _ \\ '__|
{bias}/\\__/ / |_| | | | | | | |_) | \\__/\\ |_| | |_| ||  __/ |   
{bias}\\____/ \\__,_|_| |_| |_| .__/ \\____/\\__,_|\\__|\\__\\___|_|   
{bias}                      | |                                 
{bias}                      |_|                                
{bias}
{bias}                   + MMV Extension +
{"="*self.terminal_width}
"""
        logging.info(message)
        from mmv.extra.extra_jumpcutter import JumpCutter
        return JumpCutter(ffmpeg_wrapper=self.get_ffmpeg_wrapper())

    # Main interface class, mainly sets up root dirs, get config, distributes classes
    # Send platform = "windows", "macos", "linux" for forcing a specific one
    def __init__(self, platform=None, **kwargs) -> None:
        debug_prefix = "[MMVPackageInterface.__init__]"

        self.version = "3.2: rolling"

        # Where this file is located, please refer using this on the whole package
        # Refer to it as self.mmv_skia_main.MMV_PACKAGE_ROOT at any depth in the code
        # This deals with the case we used pyinstaller and it'll get the executable path instead
        if getattr(sys, 'frozen', True):
            self.MMV_PACKAGE_ROOT = Path(
                os.path.dirname(os.path.abspath(__file__)))
            print(f"{debug_prefix} Running directly from source code")
        else:
            self.MMV_PACKAGE_ROOT = Path(
                os.path.dirname(os.path.abspath(sys.executable)))
            print(f"{debug_prefix} Running from release (sys.executable..?)")

        print(
            f"{debug_prefix} Modular Music Visualizer Python package [__init__.py] or executable located at [{self.MMV_PACKAGE_ROOT}]"
        )

        # # Load prelude configuration

        print(f"{debug_prefix} Loading prelude configuration file")

        # Build the path the prelude file should be located at
        prelude_file = self.MMV_PACKAGE_ROOT / "prelude.toml"
        print(
            f"{debug_prefix} Attempting to load prelude file located at [{prelude_file}], we cannot continue if this is wrong.."
        )

        # Load the prelude file
        with open(prelude_file, "r") as f:
            self.prelude = toml.loads(f.read())

        print(
            f"{debug_prefix} Loaded prelude configuration file, data: [{self.prelude}]"
        )

        # # # Logging

        # # We can now set up logging as we have where this file is located at

        # # Reset current handlers if any
        print(
            f"{debug_prefix} Resetting Python's logging logger handlers to empty list"
        )

        # Get logger and empty the list
        logger = logging.getLogger()
        logger.handlers = []

        # Handlers on logging to file and shell output, the first one if the user says to
        handlers = [logging.StreamHandler(sys.stdout)]

        # Loglevel is defined in the prelude.toml configuration
        LOG_LEVEL = {
            "critical": logging.CRITICAL,
            "debug": logging.DEBUG,
            "error": logging.ERROR,
            "info": logging.INFO,
            "warn": logging.warning,
            "notset": logging.NOTSET,
        }.get(self.prelude["logging"]["log_level"])

        # If user chose to log to a file, add its handler..
        if self.prelude["logging"]["log_to_file"]:

            # Hard coded where the log file will be located
            # this is only valid for the last time we run this software
            self.LOG_FILE = self.MMV_PACKAGE_ROOT / "last_log.log"

            # Reset the log file
            with open(self.LOG_FILE, "w") as f:
                print(
                    f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]"
                )
                f.write("")

            # Verbose and append the file handler
            print(
                f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]")
            handlers.append(
                logging.FileHandler(filename=self.LOG_FILE, encoding='utf-8'))

        # .. otherwise just keep the StreamHandler to stdout

        log_format = {
            "informational":
            "[%(levelname)-8s] [%(filename)-32s:%(lineno)-3d] (%(relativeCreated)-6d) %(message)s",
            "pretty":
            "[%(levelname)-8s] (%(relativeCreated)-5d)ms %(message)s",
            "economic":
            "[%(levelname)s::%(filename)s::%(lineno)d] %(message)s",
            "onlymessage": "%(message)s"
        }.get(self.prelude["logging"]["log_format"])

        # Start the logging global class, output to file and stdout
        logging.basicConfig(
            level=LOG_LEVEL,
            format=log_format,
            handlers=handlers,
        )

        # Greeter message :)
        self.greeter_message()

        # Start logging message
        bias = " " * ((self.terminal_width // 2) - 13)
        print(f"{bias[:-1]}# # [ Start Logging ] # #\n")
        print("-" * self.terminal_width + "\n")

        # Log what we'll do next
        logging.info(
            f"{debug_prefix} We're done with the pre configuration of Python's behavior and loading prelude.toml configuration file"
        )

        # Log precise Python version
        sysversion = sys.version.replace("\n", " ").replace("  ", " ")
        logging.info(f"{debug_prefix} Running on Python: [{sysversion}]")

        # # The operating system we're on, one of "linux", "windows", "macos"

        # Get the desired name from a dict matching against os.name
        if platform is None:
            self.os = {
                "posix": "linux",
                "nt": "windows",
                "darwin": "macos"
            }.get(os.name)
        else:
            logging.info(
                f"{debug_prefix} Overriding platform OS to = [{platform}]")
            self.os = platform

        # Log which OS we're running
        logging.info(
            f"{debug_prefix} Running Modular Music Visualizer on Operating System: [{self.os}]"
        )

        # # Create interface's classes

        logging.info(f"{debug_prefix} Creating Utils() class")
        self.utils = Utils()

        logging.info(f"{debug_prefix} Creating Download() class")
        self.download = Download()

        # # Common directories between packages

        # Externals
        self.externals_dir = self.MMV_PACKAGE_ROOT / "externals"
        self.externals_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Externals dir is [{self.externals_dir}]")

        # Downloads (inside externals)
        self.downloads_dir = self.MMV_PACKAGE_ROOT / "externals" / "downloads"
        self.downloads_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Downloads dir is [{self.downloads_dir}]")

        # Assets dir
        self.assets_dir = self.MMV_PACKAGE_ROOT / "assets"
        self.assets_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Assets dir is [{self.assets_dir}]")

        # Data dir
        self.data_dir = self.MMV_PACKAGE_ROOT / "data"
        self.data_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Data dir is [{self.data_dir}]")

        # Shaders dir
        self.shaders_dir = self.MMV_PACKAGE_ROOT / "shaders"
        self.shaders_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Shaders dir is [{self.shaders_dir}]")

        # Screenshots dir
        self.screenshots_dir = self.MMV_PACKAGE_ROOT / "screenshots"
        self.screenshots_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Shaders dir is [{self.screenshots_dir}]")

        # Runtime dir
        self.runtime_dir = self.MMV_PACKAGE_ROOT / "runtime"
        logging.info(
            f"{debug_prefix} Runtime dir is [{self.runtime_dir}], deleting..")
        shutil.rmtree(self.runtime_dir, ignore_errors=True)
        self.runtime_dir.mkdir(parents=True, exist_ok=True)

        # Windoe juuuust in case
        if self.os == "windows":
            logging.info(
                f"{debug_prefix} Appending the Externals directory to system path juuuust in case..."
            )
            sys.path.append(self.externals_dir)

        # # Common files

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)

        # # External dependencies where to append for PATH

        # Externals directory for Linux
        self.externals_dir_linux = self.MMV_PACKAGE_ROOT / "externals" / "linux"
        if self.os == "linux":
            logging.info(
                f"{debug_prefix} Externals directory for Linux OS is [{self.externals_dir_linux}]"
            )
            self.externals_dir_linux.mkdir(parents=True, exist_ok=True)

        # Externals directory for Windows
        self.externals_dir_windows = self.MMV_PACKAGE_ROOT / "externals" / "windows"
        if self.os == "windows":
            logging.info(
                f"{debug_prefix} Externals directory for Windows OS is [{self.externals_dir_windows}]"
            )
            self.externals_dir_windows.mkdir(parents=True, exist_ok=True)

        # Externals directory for macOS
        self.externals_dir_macos = self.MMV_PACKAGE_ROOT / "externals" / "macos"
        if self.os == "macos":
            logging.info(
                f"{debug_prefix} Externals directory for Darwin OS (macOS) is [{self.externals_dir_macos}]"
            )
            self.externals_dir_macos.mkdir(parents=True, exist_ok=True)

        # # This native platform externals dir
        self.externals_dir_this_platform = self.__get_platform_external_dir(
            self.os)
        logging.info(
            f"{debug_prefix} This platform externals directory is: [{self.externals_dir_this_platform}]"
        )

        # Update the externals search path (create one in this case)
        self.update_externals_search_path()

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)

    # Get the target externals dir for this platform
    def __get_platform_external_dir(self, platform):
        debug_prefix = "[MMVPackageInterface.__get_platform_external_dir]"

        # # This platform externals dir
        externals_dir = {
            "linux": self.externals_dir_linux,
            "windows": self.externals_dir_windows,
            "macos": self.externals_dir_macos,
        }.get(platform)

        # mkdir dne just in case cause we asked for this?
        externals_dir.mkdir(parents=True, exist_ok=True)

        # log action
        logging.info(
            f"{debug_prefix} Return external dir for platform [{platform}] -> [{externals_dir}]"
        )
        return externals_dir

    # Update the self.EXTERNALS_SEARCH_PATH to every recursive subdirectory on the platform's externals dir
    def update_externals_search_path(self):
        debug_prefix = "[MMVPackageInterface.update_externals_search_path]"

        # The subdirectories on this platform externals folder
        externals_subdirs = self.utils.get_recursively_all_subdirectories(
            self.externals_dir_this_platform)

        # When using some function like Utils.get_executable_with_name, it have an argument
        # called extra_paths, add this for searching for the full externals directory.
        # Preferably use this interface methods like find_binary instead
        self.EXTERNALS_SEARCH_PATH = [self.externals_dir_this_platform]

        # If we do have subdirectories on this platform externals then append to it
        if externals_subdirs:
            self.EXTERNALS_SEARCH_PATH += externals_subdirs

    # Search for something in system's PATH, also searches for the externals folder
    # Don't append the extra .exe because Linux, macOS doesn't have these, returns False if no binary was found
    def find_binary(self, binary):
        debug_prefix = "[MMVPackageInterface.find_binary]"

        # Append .exe for Windows
        if (self.os == "windows") and (not binary.endswith(".exe")):
            binary += ".exe"

        # Log action
        logging.info(
            f"{debug_prefix} Finding binary in PATH and EXTERNALS directories: [{binary}]"
        )
        return self.utils.get_executable_with_name(
            binary, extra_paths=self.EXTERNALS_SEARCH_PATH)

    # Make sure we have some target Externals, downloads latest release for them.
    # For forcing to download the Windows binaries for a release, send platform="windows" for overwriting
    # otherwise it'll be set to this class's os.
    #
    # For FFmpeg, mpv: Linux and macOS people please install from your distro's package manager.
    #
    # Possible values for target are: ["ffmpeg", "mpv", "musescore"]
    #
    def check_download_externals(self, target_externals=[], platform=None):
        debug_prefix = "[MMVPackageInterface.check_download_externals]"

        # Overwrite os if user set to a specific one
        if platform is None:
            platform = self.os
        else:
            # Error assertion, only allow linux, macos or windows target os
            valid = ["linux", "macos", "windows"]
            if not platform in valid:
                err = f"Target os [{platform}] not valid: should be one of {valid}"
                logging.error(f"{debug_prefix} {err}")
                raise RuntimeError(err)

        # Force the externals argument to be a list
        target_externals = self.utils.force_list(target_externals)

        # Log action
        logging.info(
            f"{debug_prefix} Checking externals {target_externals} for os = [{platform}]"
        )

        # We're frozen (running from release..)
        if getattr(sys, 'frozen', False):
            logging.info(
                f"{debug_prefix} Not checking for externals because is executable build.. (should have them bundled?)"
            )
            return

        # Short hand
        sep = os.path.sep

        # The target externals dir for this platform, it must be windows if we're here..
        target_externals_dir = self.__get_platform_external_dir(platform)

        # For each target external
        for external in target_externals:
            debug_prefix = "[MMVPackageInterface.check_download_externals]"
            logging.info(
                f"{debug_prefix} Checking / downloading external: [{external}] for platform [{platform}]"
            )

            # # FFmpeg / FFprobe

            if external == "ffmpeg":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(binary="ffmpeg")
                    continue

                # If we don't have FFmpeg binary on externals dir
                if not self.find_binary("ffmpeg.exe"):

                    # Get the latest release number of ffmpeg
                    repo = "https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest"
                    logging.info(
                        f"{debug_prefix} Getting latest release info on repository: [{repo}]"
                    )
                    ffmpeg_release = json.loads(
                        self.download.get_html_content(repo))

                    # The assets (downloadable stuff)
                    assets = ffmpeg_release["assets"]
                    logging.info(
                        f"{debug_prefix} Available assets to download (checking for non shared, gpl, non vulkan release):"
                    )

                    # Parsing the version we target and want
                    for item in assets:

                        # The name of the
                        name = item["name"]
                        logging.info(f"{debug_prefix} - [{name}]")

                        # Expected stuff
                        is_lgpl = "lgpl" in name
                        is_shared = "shared" in name
                        have_vulkan = "vulkan" in name
                        from_master = "N" in name

                        # Log what we expect
                        logging.info(
                            f"{debug_prefix} - :: Is LGPL:                   [{is_lgpl:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Is Shared:                 [{is_shared:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Have Vulkan:               [{have_vulkan:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Master branch (N in name): [{from_master:<1}] (expect: 0)"
                        )

                        # We have a match!
                        if not (is_lgpl + is_shared + have_vulkan +
                                from_master):
                            logging.info(
                                f"{debug_prefix} - >> :: We have a match!!")
                            download_url = item["browser_download_url"]
                            break

                    # Where we'll download from
                    logging.info(
                        f"{debug_prefix} Download URL: [{download_url}]")

                    # Where we'll save the compressed zip of FFmpeg
                    ffmpeg_zip = self.downloads_dir + f"{sep}{name}"

                    # Download FFmpeg build
                    self.download.wget(download_url, ffmpeg_zip,
                                       f"FFmpeg v={name}")

                    # Extract the files
                    self.download.extract_zip(ffmpeg_zip, target_externals_dir)

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [ffmpeg] binary in externals / system path!!"
                    )

            # # MPV FIXME: deprecate future version

            if external == "mpv":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(
                        binary="mpv",
                        help_fix=f"Visit [https://mpv.io/installation/]")
                    continue

                # If we don't have mpv binary on externals dir or system's path
                if not self.find_binary("mpv"):

                    mpv_7z_version = "mpv-x86_64-20201220-git-dde0189.7z"

                    # Where we'll save the compressed zip of FFmpeg
                    mpv_7z = self.downloads_dir + f"{sep}{mpv_7z_version}"

                    # Download mpv build
                    self.download.wget(
                        f"https://sourceforge.net/projects/mpv-player-windows/files/64bit/{mpv_7z_version}/download",
                        mpv_7z, f"MPV v=20201220-git-dde0189")

                    # Where to extract final mpv
                    mpv_extracted_folder = f"{self.externals_dir_this_platform}{sep}" + mpv_7z_version.replace(
                        ".7z", "")
                    self.utils.mkdir_dne(path=mpv_extracted_folder)

                    # Extract the files
                    self.download.extract_file(mpv_7z, mpv_extracted_folder)

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [mpv] binary in externals / system path!!"
                    )

            # # Musescore

            if external == "musescore":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(
                        binary="musescore",
                        help_fix=
                        f"Go to [https://musescore.org/en/download] and install for your platform"
                    )
                    continue

                # If we don't have musescore binary on externals dir or system's path
                if not self.find_binary("musescore"):

                    # Version we want
                    musescore_version = "v3.5.2/MuseScorePortable-3.5.2.311459983-x86.paf.exe"

                    # Download musescore
                    self.download.wget(
                        f"https://cdn.jsdelivr.net/musescore/{musescore_version}",
                        f"{self.externals_dir_this_platform}{sep}musescore.exe",
                        f"Musescore Portable v=[{musescore_version}]")

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [musescore] binary in externals / system path!!"
                    )

            # Update the externals search path because we downloaded stuff
            self.update_externals_search_path()

    # Ensure we have an external dependency we can't micro manage because too much entropy
    def __cant_micro_manage_external_for_you(self, binary, help_fix=None):
        debug_prefix = "[MMVPackageInterface.__cant_micro_manage_external_for_you]"

        logging.warning(
            f"{debug_prefix} You are using Linux or macOS, please make sure you have [{binary}] package binary installed on your distro or on homebrew, we'll just check for it now, can't continue if you don't have it.."
        )

        # Can't continue
        if not self.find_binary(binary):
            logging.error(
                f"{debug_prefix} Couldn't find lowercase [{binary}] binary on PATH, install from your Linux distro package manager / macOS homebrew, please install it"
            )

            # Log any extra help we give the user
            if help_fix is not None:
                logging.error(f"{debug_prefix} {help_fix}")

            # Exit with non zero error code
            sys.exit(-1)