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.
157 lines
7.2 KiB
157 lines
7.2 KiB
#! /usr/bi n/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2020, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
|
|
This application 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")
|
|
|
|
#Standard Library
|
|
from datetime import datetime
|
|
import pathlib
|
|
import os
|
|
#Our Modules
|
|
from engine.start import PATHS
|
|
|
|
def fast_scandir(dir):
|
|
"""
|
|
Get all subdirectories recursively.
|
|
https://stackoverflow.com/questions/973473/getting-a-list-of-all-subdirectories-in-the-current-directory"""
|
|
try:
|
|
subfolders= [f.path for f in os.scandir(dir) if f.is_dir()]
|
|
except PermissionError:
|
|
subfolders = []
|
|
|
|
for dir in list(subfolders):
|
|
subfolders.extend(fast_scandir(dir))
|
|
return subfolders
|
|
|
|
|
|
class Watcher(object):
|
|
"""
|
|
Initialize with the server controller
|
|
|
|
The watcher will should only run when the program is in the mode to choose a session.
|
|
If a session is loaded it will technically work, but will be a waste of resources.
|
|
|
|
The watcher will also trigger the nsmController to redundantly query for a session list,
|
|
for example when a new empty sessions gets created or duplicated. Once because nsmd sends the
|
|
"new" signal, and then our watcher will notice the change itself. However, this happens only so
|
|
often and can be accepted, so the program does not need more checks and exceptions.
|
|
|
|
Inform you via callback when:
|
|
|
|
a) a session dir was deleted
|
|
b) a new directory got created.
|
|
c) session directory renamed or moved
|
|
d) session.nsm changed timestamp
|
|
e) A lockfile appeared or disappeared
|
|
|
|
We cannot poll nsmServerControl.exportSessionsAsDicts() because that triggers an nsmd response
|
|
including log message and will flood their console. Instead this class only offers incremental
|
|
updates.
|
|
Another way is to watch the dir for changes ourselves in in some cases request a new project
|
|
list from nsmd.
|
|
|
|
Lockfile functionality goes beyond what NSM offers. A session-daemon can open only one
|
|
project at a time. If you try to open another project with a second GUI (but same NSM-URL)
|
|
it goes wrong a bit, at least in the non-session-manager GUI. They will leave a zombie lockfile.
|
|
However, it still is possible to open a second nsmd instance. In this case the lockfile prevents
|
|
opening a session twice. And we are reflecting the opened state of the project, no matter from
|
|
which daemon. Horray for us :)
|
|
|
|
Clients can only be added or removed while a session is locked. We do not check nor update
|
|
number of clients or symlinks. Therefore we advice a GUI to deactivate the display of
|
|
these two values while a session is locked (together with size)
|
|
"""
|
|
|
|
def __init__(self, nsmServerControl):
|
|
self.active = True
|
|
|
|
self._nsmServerControl = nsmServerControl
|
|
|
|
assert self._nsmServerControl.sessionRoot
|
|
|
|
self._directories = fast_scandir(self._nsmServerControl.sessionRoot)
|
|
|
|
logger.info("Requestion our own copy of the session list. Don't worry about the apparent redundant call :)")
|
|
self._lastExport = self._nsmServerControl.exportSessionsAsDicts()
|
|
self._lastTimestamp = {d["nsmSessionName"]:d["lastSavedDate"] for d in self._lastExport} #str:str
|
|
#Init all values with None will send the initial state via callback on program start, which is what the user wants to know.
|
|
self._lastLockfile = {d["nsmSessionName"]:None for d in self._lastExport} #str:bool
|
|
|
|
self.timeStampHook = None # a single function that gets informed of changes, most likely the api callback
|
|
self.lockFileHook = None # a single function that gets informed of changes, most likely the api callback
|
|
self.sessionsChangedHook = None # the api callback function api.callbacks._sessionsChanged. Rarely used.
|
|
|
|
def resume(self, *args):
|
|
"""For api callbacks"""
|
|
self.active = True
|
|
#If we returned from an open session that will surely have changed. Trigger a single poll
|
|
self.process()
|
|
logger.info("Watcher resumed")
|
|
|
|
def suspend(self, *args):
|
|
"""For api callbacks"""
|
|
self.active = False
|
|
logger.info("Watcher suspended")
|
|
|
|
def process(self):
|
|
"""Add this to your event loop.
|
|
|
|
We look for any changes in the directory structure.
|
|
If we detect any we simply trigger a new NSM export and a new NSM generated project
|
|
list via callback. We do not expect this to happen often.
|
|
|
|
This will also trigger if we add a new session ourselves. This *is* our way to react to
|
|
new Sessions.
|
|
"""
|
|
if not self.active:
|
|
return
|
|
|
|
if self.sessionsChangedHook:
|
|
current_directories = fast_scandir(self._nsmServerControl.sessionRoot)
|
|
|
|
if not self._directories == current_directories:
|
|
self._directories = current_directories
|
|
self._lastExport = self.sessionsChangedHook() #will gather its own data, send it to api callbacks, but also return for us.
|
|
self._lastTimestamp = {d["nsmSessionName"]:d["lastSavedDate"] for d in self._lastExport} #str:str
|
|
self._lastLockfile = {d["nsmSessionName"]:None for d in self._lastExport} #str:bool
|
|
|
|
#Now check the incremental hooks.
|
|
#No hooks, no reason to process
|
|
if not (self.timeStampHook or self.lockFileHook):
|
|
logger.info("No watcher-hooks to process")
|
|
return
|
|
|
|
for entry in self._lastExport:
|
|
nsmSessionName = entry["nsmSessionName"]
|
|
|
|
#Timestamp of session.nsm
|
|
if self.timeStampHook:
|
|
timestamp = datetime.fromtimestamp(entry["sessionFile"].stat().st_mtime).isoformat(sep=" ", timespec='minutes') #same format as server control export
|
|
if not timestamp == self._lastTimestamp[nsmSessionName]: #This will only trigger on a minute-based slot, which is all we want and need. This is for relaying information to the user, not for advanced processing.
|
|
self._lastTimestamp[nsmSessionName] = timestamp
|
|
self.timeStampHook(nsmSessionName, timestamp)
|
|
|
|
#Lockfiles
|
|
if self.lockFileHook:
|
|
lockfileState = entry["lockFile"].is_file()
|
|
if not self._lastLockfile[nsmSessionName] == lockfileState:
|
|
self._lastLockfile[nsmSessionName] = lockfileState
|
|
self.lockFileHook(nsmSessionName, lockfileState)
|
|
|