Music production session manager
https://www.laborejo.org
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
9.5 KiB
195 lines
9.5 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
|
|
This is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import logging; logger = logging.getLogger(__name__); logger.info("import")
|
|
|
|
import pathlib
|
|
import configparser
|
|
import os
|
|
import stat
|
|
|
|
import xdg.DesktopEntry #pyxdg https://www.freedesktop.org/wiki/Software/pyxdg/
|
|
import xdg.IconTheme #pyxdg https://www.freedesktop.org/wiki/Software/pyxdg/
|
|
|
|
from engine.start import PATHS
|
|
|
|
def nothing(*args): pass
|
|
|
|
class SupportedProgramsDatabase(object):
|
|
"""Find all binaries with NSM support. Resources are:
|
|
* Agordejo internal program list of known working programs.
|
|
* Internal blacklist of known redundant programs (such as non-daw) or nonsense entries, like Agordejo itself
|
|
* Finally, local to a users system: User whitelist for any program, user blacklist.
|
|
Those two have the highest priority.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.progressHook = nothing #prevents the initial programstart from sending meaningless messages for the cached data. Set and reverted in self.build
|
|
|
|
self.blackList = set(("nsmd", "non-daw", "carla", "agordejo", "adljack", "agordejo.bin")) #only programs that have to do with audio and music. There is another general blacklist that speeds up discovery
|
|
self.whiteList = set(("thisdoesnotexisttest", "patroneo", "vico", "tembro", "laborejo",
|
|
"fluajho", "carla-rack", "carla-patchbay", "carla-jack-multi", "carla-jack-single",
|
|
"ardour5", "ardour6", "nsm-data", "jackpatch", "nsm-proxy", "ADLplug", "ams",
|
|
"drumkv1_jack", "synthv1_jack", "samplv1_jack", "padthv1_jack",
|
|
"luppp", "non-mixer", "non-timeline", "non-sequencer", "non-midi-mapper", "non-mixer-noui",
|
|
"OPNplug", "qmidiarp", "qtractor", "zynaddsubfx", "jack_mixer",
|
|
"hydrogen", "mfp", "shuriken", "guitarix", "radium",
|
|
"ray-proxy", "ray-jackpatch", "amsynth", "mamba", "qseq66", "zynaddsubfx-jack"
|
|
)) #shortcut list and programs not found by buildCache_grepExecutablePaths because they are just shellscripts and do not contain /nsm/server/announce.
|
|
self.userWhitelist = () #added dynamically by api.systemProgramsSetWhitelist add to morePrograms. highest priority
|
|
self.userBlacklist = () #added dynamically by api.systemProgramsSetBlacklist as blacklist. highest priority
|
|
self.programs = [] #main data structure of this file. list of dicts. guaranteed keys: agordejoExec, name, agordejoFullPath. And probably others, like description and version.
|
|
self.nsmExecutables = set() #set of executables for fast membership, if a GUI wants to know if they are available. Needs to be build "manually" with self.programs. no auto-property for a list. at least we don't want to do the work.
|
|
self.unfilteredExecutables = None #in build()
|
|
|
|
#self.build() needs to be called when the program is ready, e.g. a GUI is set up and has the progressHook ready
|
|
|
|
|
|
def _isexe(self, path):
|
|
"""executable by owner"""
|
|
return path.is_file() and stat.S_IXUSR & os.stat(path)[stat.ST_MODE] == 64
|
|
|
|
def _executableNameToFullPath(self, exeName:str, executableSystemPaths:set)->pathlib.Path:
|
|
for directory in executableSystemPaths:
|
|
p = pathlib.Path(directory, exeName)
|
|
if p.exists():
|
|
return p
|
|
else:
|
|
return None
|
|
|
|
def gatherAllNsmClients(self)->list:
|
|
"""
|
|
We parse .desktop files for the nsm flag
|
|
and export our own list of dicts with various entries. see below.
|
|
"""
|
|
|
|
executableSystemPaths = set([pathlib.Path(p).resolve() for p in os.environ["PATH"].split(os.pathsep)]) #resolve filters out symlinks, like ArchLinux's /sbin and /bin. set() makes it unique
|
|
|
|
"""Go through all dirs including subdirs"""
|
|
xdgPaths = (
|
|
pathlib.Path("/usr/share/applications"),
|
|
pathlib.Path("/usr/local/share/applications"),
|
|
pathlib.Path(pathlib.Path.home(), ".local/share/applications"),
|
|
)
|
|
self.progressHook("")
|
|
result = []
|
|
|
|
for basePath in xdgPaths:
|
|
try: #TODO: this whole part of the program is a mess. Confiparser and Qt in a bundle segfault too often.
|
|
self.progressHook(f"{basePath}")
|
|
except Exception as e:
|
|
logger.error(e)
|
|
pass
|
|
for f in basePath.glob('**/*'):
|
|
if f.is_file() and f.suffix == ".desktop":
|
|
desktopEntry = xdg.DesktopEntry.DesktopEntry(f)
|
|
|
|
"""
|
|
#Don't validate. This is over-zealous and will mark deprecation and unknown categories.
|
|
try:
|
|
desktopEntry.validate()
|
|
except xdg.Exceptions.ValidationError as e:
|
|
logger.error(f"Desktop file {f} has problems: {e}")
|
|
continue
|
|
"""
|
|
|
|
|
|
agorExec = desktopEntry.get("X-NSM-Exec")
|
|
if not agorExec:
|
|
n = pathlib.Path(desktopEntry.getExec()).name
|
|
agorExec = n.split(" ")[0].strip() # this will fail with special filenames, such as spaces in filenames. But it is already the fallback for programs not adhering to the nsm specs. not our problem anymore.
|
|
|
|
blacklisted = agorExec in self.blackList or agorExec in self.userBlacklist
|
|
|
|
if blacklisted:
|
|
logger.info(f"{agorExec}] is blacklisted. Skip.")
|
|
continue
|
|
|
|
isNSM = ( bool(desktopEntry.get("X-NSM-Capable"))
|
|
or bool(desktopEntry.get("X-NSM-capable"))
|
|
or agorExec in self.whiteList
|
|
or agorExec in self.userWhitelist
|
|
)
|
|
|
|
|
|
if isNSM:
|
|
absExecPath = self._executableNameToFullPath(agorExec, executableSystemPaths)
|
|
if absExecPath is None:
|
|
logger.warning(f"Couldn't find actual path for {agorExec} eventhough we searched with the name from it's desktop file. If this program is not installed at all this is a false-negative error. Don't worry.")
|
|
continue
|
|
|
|
if not self._isexe(absExecPath):
|
|
logger.error(f"{absExecPath} was derived from .desktop file and exist, but it not executable!")
|
|
continue
|
|
|
|
data = {
|
|
"agordejoName" : desktopEntry.getName(),
|
|
"agordejoExec" : agorExec , #to prevent 'carla-rack %u'. This is what nsm will call.
|
|
"agordejoIconPath" : xdg.IconTheme.getIconPath(desktopEntry.getIcon()),
|
|
"agordejoFullPath" : absExecPath, #This is only for information. nsm calls agordejoExec
|
|
"agordejoDescription" : desktopEntry.getComment(),
|
|
}
|
|
result.append(data)
|
|
self.progressHook("")
|
|
return result
|
|
|
|
|
|
def build(self, progressHook=None):
|
|
"""Can be called at any time by the user to update after installing new programs"""
|
|
|
|
if progressHook:
|
|
#receives one string which indicates what files is currently parsed.
|
|
#Just the pure path, this will not get translated!
|
|
#The purpose is to show a "we are not frozen!" feedback to the user.
|
|
#It doesn't really matter what is reported back as long as it changes often
|
|
self.progressHook = progressHook
|
|
|
|
logger.info("Building launcher database. This might take a minute")
|
|
self.progressHook("")
|
|
self.programs = self.gatherAllNsmClients()
|
|
self.progressHook("")
|
|
self.unfilteredExecutables = self.buildCache_unfilteredExecutables()
|
|
self.nsmExecutables = set(d["agordejoExec"] for d in self.programs)
|
|
self.progressHook("")
|
|
self.progressHook = nothing
|
|
logger.info("Building launcher database done.")
|
|
|
|
def getNsmClients(self)->list:
|
|
"""Return the main data structure of this file:
|
|
a list of dicts
|
|
"""
|
|
return self.programs
|
|
|
|
|
|
def buildCache_unfilteredExecutables(self):
|
|
"""Just a list of all exectuables of this systems PATH.
|
|
This is used for the GUIs "start any program" with auto completion.
|
|
"""
|
|
|
|
result = []
|
|
executablePaths = [pathlib.Path(p) for p in os.environ["PATH"].split(os.pathsep)]
|
|
for path in executablePaths:
|
|
self.progressHook(f"{path}")
|
|
result += [str(pathlib.Path(f).relative_to(path)) for f in path.glob("*") if self._isexe(f)]
|
|
|
|
return sorted(list(set(result)))
|
|
|
|
programDatabase = SupportedProgramsDatabase()
|
|
|