# MusicDB, a music manager with web-bases UI that focus on music.
# Copyright (C) 2017 - 2021 Ralf Stemmer <ralf.stemmer@gmx.net>
#
# This program 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/>.
"""
All sections and options can be accessed by their names: ``MusicDBConfig(...).section.option``.
Details of the configurations possible for MusicDB can be found in :doc:`/basics/config`.
To create a new entry for the MusicDB Configuration the following steps must be done:
#. Set the section and option in the configuration file as well as in the template in the share directory
#. In case a new section shall be created, create an empty class for this section inside this module
#. Read the option in the constructor of the :class:`~musicdb.lib.cfg.musicdb.MusicDBConfig` class
For example, the following option shall be added:
.. code-block:: ini
[newmod]
enable = True
The option must be read inside the :class:`~musicdb.lib.cfg.musicdb.MusicDBConfig` ``__init__`` method.
The read value must be written into an attribute named as the option, inside an instance of the dummy class named liked the section:
.. code-block:: python
self.newmod = SECTION()
self.newmod.enable = self.Get(bool, "newmod", "enable", False)
Now, the new option is available in the configuration object created using this class:
.. code-block:: python
cfg = MusicDBConfig("musicdb.ini")
if cfg.newmod.enable:
print("newmod enabled!")
"""
import logging
import grp
import pwd
import stat
from musicdb.lib.cfg.config import Config
from musicdb.lib.filesystem import Filesystem
class META:
pass
class SERVER:
pass
class WEBSOCKET:
pass
class SOCKET:
pass
class TLS:
pass
class DATABASE:
pass
class MUSIC:
pass
class ARTWORK:
pass
class VIDEOFRAMES:
pass
class UPLOAD:
pass
class EXTERN:
pass
class TRACKER:
pass
class ICECAST:
pass
class LOG:
pass
class DEBUG:
pass
class RANDY:
pass
class SECTION:
pass
[docs]class MusicDBConfig(Config):
"""
This class provides the access to the MusicDB configuration file.
By default, the configuration will be read from ``/etc/musicdb.ini``.
Args:
path (str): Optional alternative configuration path. Default is ``/etc/musicdb.ini``
"""
def __init__(self, path="/etc/musicdb.ini"):
Config.__init__(self, path)
self.fs = Filesystem("/")
logging.info("Reading and checking MusicDB Configuration at \033[0;36m%s", path)
# [meta]
self.meta = META()
self.meta.version = self.Get(int, "meta", "version", 1)
if self.meta.version < 6:
logging.warning("Version of \"%s\" is too old. Please update the MusicDB Configuration!", path)
# [musicdb]
self.musicdb = SECTION()
self.musicdb.username = self.Get(str, "musicdb", "username", "musicdb")
self.musicdb.groupname = self.Get(str, "musicdb", "groupname", "musicdb")
try:
pwd.getpwnam(self.musicdb.username)
except KeyError:
logging.warning("The user name \"%s\" for [musicdb]->username is not an existing UNIX user!", self.musicdb.username)
try:
grp.getgrnam(self.musicdb.groupname)
except KeyError:
logging.warning("The group name \"%s\" for [musicdb]->groupname is not an existing UNIX group!", self.musicdb.groupname)
# [directories]
self.directories = SECTION()
self.directories.music = self.GetDirectory("directories", "music", "/var/music")
self.directories.data = self.GetDirectory("directories", "data", "/var/lib/musicdb")
self.directories.webdata = self.directories.data + "/webdata"
self.directories.uploads = self.directories.data + "/uploads"
self.directories.tasks = self.directories.data + "/tasks"
self.directories.state = self.directories.data + "/state"
self.directories.config = self.directories.data + "/config"
self.directories.share = "/usr/share/musicdb"
self.directories.artwork = self.directories.webdata + "/artwork"
# files
self.files = SECTION()
self.files.webuiconfig = self.directories.config + "/webui.ini"
self.files.wsapikey = self.directories.config + "/wsapikey.txt"
self.files.musicdatabase = self.directories.data + "/music.db"
self.files.trackerdatabase = self.directories.data + "/tracker.db"
self.files.defaultalbumcover= self.directories.artwork + "/default.jpg"
self.files.webuijsconfig = self.directories.webdata + "/config.js"
# [log]
self.log = SECTION()
self.log.logfile = self.Get(str, "log", "logfile", "journal")
self.log.loglevel = self.Get(str, "log", "loglevel", "WARNING").upper()
if not self.log.loglevel in ["DEBUG", "INFO", "WARNING", "ERROR"]:
logging.error("Invalid loglevel for [log]->loglevel. Loglevel must be one of the following: DEBUG, INFO, WARNING, ERROR")
self.log.debugfile = self.Get(str, "log", "debugfile", None)
if self.log.debugfile == "/dev/null":
self.log.debugfile = None
self.log.ignore = self.Get(str, "log", "ignore", None, islist=True)
# [debug]
self.debug = SECTION()
self.debug.disablestats = self.Get(bool, "debug", "disablestats", False)
self.debug.disabletracker = self.Get(bool, "debug", "disabletracker", False)
self.debug.disableai = self.Get(bool, "debug", "disableai", True)
self.debug.disabletagging = self.Get(bool, "debug", "disabletagging", False)
self.debug.disableicecast = self.Get(bool, "debug", "disableicecast", False)
self.debug.disablevideos = self.Get(bool, "debug", "disablevideos", False)
# [websocket]
self.websocket = SECTION()
self.websocket.bind = self.Get(str, "websocket", "bind", "127.0.0.1")
self.websocket.port = self.Get(int, "websocket", "port", 9000)
self.websocket.opentimeout = self.Get(int, "websocket", "opentimeout", 10)
self.websocket.closetimeout = self.Get(int, "websocket", "closetimeout", 5)
self.websocket.cert = self.Get(str, "websocket", "cert", self.directories.data + "websocket.cert")
self.websocket.key = self.Get(str, "websocket", "key", self.directories.data + "websocket.key")
# The certificate and key files are validated in detail when MusicDB starts. No need to check them here.
# [uploads]
self.uploads = SECTION()
self.uploads.allow = self.Get(str, "uploads", "allow", "artwork, songs", islist=True)
# [tracker]
self.tracker = SECTION()
self.tracker.cuttime = self.Get(int, "tracker", "cuttime", 30)
self.tracker.trackrandom = self.Get(bool, "tracker", "trackrandom", False)
# [Icecast]
self.icecast = SECTION()
self.icecast.port = self.Get(int, "Icecast", "port", 6666)
self.icecast.user = self.Get(str, "Icecast", "user", "source")
self.icecast.password = self.Get(str, "Icecast", "password", None)
self.icecast.mountname = self.Get(str, "Icecast", "mountname", "/stream")
# [randy]
self.randy = SECTION()
self.randy.nodisabled = self.Get(bool, "randy", "nodisabled", True)
self.randy.nohated = self.Get(bool, "randy", "nohated", True)
self.randy.nohidden = self.Get(bool, "randy", "nohidden", True)
self.randy.nobadfile = self.Get(bool, "randy", "nobadfile", True)
self.randy.nolivemusic = self.Get(bool, "randy", "nolivemusic", True)
self.randy.minsonglen = self.Get(int, "randy", "minsonglen", 120)
self.randy.maxsonglen = self.Get(int, "randy", "maxsonglen", 600)
self.randy.songbllen = self.Get(int, "randy", "songbllen", 50)
self.randy.albumbllen = self.Get(int, "randy", "albumbllen", 20)
self.randy.artistbllen = self.Get(int, "randy", "artistbllen", 10)
self.randy.videobllen = self.Get(int, "randy", "videobllen", 10)
self.randy.maxblage = self.Get(int, "randy", "maxblage", 24)
self.randy.maxtries = self.Get(int, "randy", "maxtries", 10)
# [music]
self.music = SECTION()
ignorelist = self.Get(str, "music", "ignoreartists","lost+found")
ignorelist = ignorelist.split("/")
self.music.ignoreartists = [item.strip() for item in ignorelist]
ignorelist = self.Get(str, "music", "ignorealbums", "")
ignorelist = ignorelist.split("/")
self.music.ignorealbums = [item.strip() for item in ignorelist]
ignorelist = self.Get(str, "music", "ignoresongs", ".directory / desktop.ini / Desktop.ini / .DS_Store / Thumbs.db")
ignorelist = ignorelist.split("/")
self.music.ignoresongs = [item.strip() for item in ignorelist]
# [albumcover]
self.albumcover = SECTION()
self.albumcover.scales = self.Get(int, "albumcover", "scales", "50, 150, 500", islist=True)
# [videoframes]
self.videoframes = SECTION()
self.videoframes.frames = self.Get(int, "videoframes", "frames", "5")
self.videoframes.previewlength = self.Get(int, "videoframes", "previewlength","3")
self.videoframes.scales = self.Get(str, "videoframes", "scales", "50x27, 150x83", islist=True)
for scale in self.videoframes.scales:
try:
width, height = map(int, scale.split("x"))
except Exception as e:
logging.error("Invalid video scale format in [videoframes]->scales: Expected format WxH, with W and H as integers. Actual format: %s.", scale)
# [extern]
self.extern = SECTION()
self.extern.configtemplate = self.GetFile( "extern", "configtemplate","/usr/share/musicdb/extconfig.ini")
self.extern.statedir = self.Get(str, "extern", "statedir", ".mdbstate")
self.extern.configfile = self.Get(str, "extern", "configfile", "config.ini")
self.extern.songmap = self.Get(str, "extern", "songmap", "songmap.csv")
[docs] def GetDirectory(self, section, option, default):
"""
This method gets a string from the configuration file and checks if it is an existing directory.
If not it prints a warning.
The \"invalid\" path will be returned anyway, because it may be OK that the directory does not exist yet.
Args:
section (str): Section of an ini-file
option (str): Option inside the section of an ini-file
default (str): Default directory path if option is not set in the file
Returns:
The value of the option set in the config-file or the default value.
"""
path = self.Get(str, section, option, default)
if not self.fs.IsDirectory(path):
logging.warning("Value of [%s]->%s=%s does not address an existing directory.", section, option, path)
return path
[docs] def GetFile(self, section, option, default):
"""
This method gets a string from the configuration file and checks if it is an existing file.
If not it prints a warning.
The \"invalid\" path will be returned anyway, because it may be OK that the file does not exist yet.
Args:
section (str): Section of an ini-file
option (str): Option inside the section of an ini-file
default (str): Default file path if option is not set in the file
Returns:
The value of the option set in the config-file or the default value.
"""
path = self.Get(str, section, option, default)
if not self.fs.IsFile(path):
logging.warning("Value of [%s]->%s=%s does not address an existing file.", section, option, path)
return path
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4