From: Marcel van der Veldt Date: Wed, 15 Apr 2020 22:40:01 +0000 (+0200) Subject: small refactor X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=423efb7c3e1a9a2caab43b9e00fb9b6de798b696;p=music-assistant-server.git small refactor --- diff --git a/mass.py b/mass.py deleted file mode 100755 index 87692611..00000000 --- a/mass.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- - -import sys -import os -import logging -from aiorun import run -import asyncio - -logger = logging.getLogger() -logformat = logging.Formatter('%(asctime)-15s %(levelname)-5s %(name)s.%(module)s -- %(message)s') -consolehandler = logging.StreamHandler() -consolehandler.setFormatter(logformat) -logger.addHandler(consolehandler) - - -def get_config(): - ''' start config handling ''' - data_dir = '' - debug = False - update_latest = False - # prefer command line args - if len(sys.argv) > 1: - data_dir = sys.argv[1] - if len(sys.argv) > 2: - debug = sys.argv[2] == "debug" - if len(sys.argv) > 3: - update_latest = sys.argv[3] == "update" - # fall back to environment variables (for plain docker) - if os.environ.get('mass_datadir'): - data_dir = os.environ['mass_datadir'] - if os.environ.get('mass_debug'): - debug = os.environ['mass_debug'].lower() != 'false' - if os.environ.get('mass_update'): - update_latest = os.environ['mass_update'].lower() != 'false' - # hassio config file found - conf_file = '/data/options.json' - if os.path.isfile(conf_file): - try: - import json - with open(conf_file) as f: - conf = json.loads(f.read()) - data_dir = conf['data_dir'] - debug = conf['debug_messages'] - update_latest = conf['use_nightly'] - except: - logger.exception('could not load options.json') - return data_dir, debug, update_latest - -def do_update(): - ''' auto update to latest git version ''' - base_dir = os.path.dirname(os.path.abspath(__file__)) - if os.path.isdir(".git") or os.path.isdir("%s/.git" % base_dir): - # dev environment - return - logger.info("Updating to latest Git version!") - import subprocess - # TODO: handle this properly - args = """ - cd %s - curl -LOks "https://github.com/marcelveldt/musicassistant/archive/master.zip" - unzip -q master.zip - pip install -r musicassistant-master/requirements.txt - cp -rf musicassistant-master/music_assistant . - cp -rf musicassistant-master/mass.py . - rm -R musicassistant-master - """ % (base_dir, ) - if subprocess.call(args, shell=True) == 0: - logger.info("Update succesfull") - else: - logger.error("Update failed - do you have curl and zip installed ?") - - -if __name__ == "__main__": - # get config - data_dir, debug, update_latest = get_config() - if update_latest: - do_update() - # create event_loop with uvloop - event_loop = asyncio.get_event_loop() - try: - import uvloop - uvloop.install() - except ImportError: - # uvloop is not available on Windows so safe to ignore this - logger.warning("uvloop support is disabled") - # config debug settings if needed - if debug: - event_loop.set_debug(True) - logger.setLevel(logging.DEBUG) - logging.getLogger('aiosqlite').setLevel(logging.INFO) - logging.getLogger('asyncio').setLevel(logging.WARNING) - else: - logger.setLevel(logging.INFO) - # start music assistant! - from music_assistant import MusicAssistant - mass = MusicAssistant(data_dir, event_loop) - run(mass.start(), loop=event_loop) - \ No newline at end of file diff --git a/music_assistant/__init__.py b/music_assistant/__init__.py index 628faf69..feeeef76 100644 --- a/music_assistant/__init__.py +++ b/music_assistant/__init__.py @@ -1,111 +1 @@ -#!/usr/bin/env python3 -# -*- coding:utf-8 -*- - -import asyncio -import re -import os -import shutil -import slugify as unicode_slug -import uuid -import json -import time -import logging -import threading - -from .database import Database -from .config import MassConfig -from .utils import run_periodic, LOGGER, try_parse_bool, serialize_values -from .metadata import MetaData -from .cache import Cache -from .music_manager import MusicManager -from .player_manager import PlayerManager -from .http_streamer import HTTPStreamer -from .homeassistant import HomeAssistant -from .web import Web - - -class MusicAssistant(): - - def __init__(self, datapath, event_loop): - ''' - Create an instance of MusicAssistant - :param datapath: file location to store the data - :param event_loop: asyncio event_loop - ''' - self.event_loop = event_loop - self.event_loop.set_exception_handler(self.handle_exception) - self.datapath = datapath - self.event_listeners = {} - self.config = MassConfig(self) - # init modules - self.db = Database(self) - self.cache = Cache(self) - self.metadata = MetaData(self) - self.web = Web(self) - self.hass = HomeAssistant(self) - self.music = MusicManager(self) - self.players = PlayerManager(self) - self.http_streamer = HTTPStreamer(self) - - async def start(self): - ''' start running the music assistant server ''' - await self.db.setup() - await self.cache.setup() - await self.metadata.setup() - await self.hass.setup() - await self.music.setup() - await self.players.setup() - await self.web.setup() - await self.http_streamer.setup() - # wait for exit - try: - while True: - await asyncio.sleep(3600) - except asyncio.CancelledError: - LOGGER.info("Application shutdown") - await self.signal_event("shutdown") - self.config.save() - await self.db.close() - await self.cache.close() - - def handle_exception(self, loop, context): - ''' global exception handler ''' - LOGGER.debug(f"Caught exception: {context}") - loop.default_exception_handler(context) - - async def signal_event(self, msg, msg_details=None): - ''' signal (systemwide) event ''' - if not (msg_details == None or isinstance(msg_details, (str, dict))): - msg_details = serialize_values(msg_details) - listeners = list(self.event_listeners.values()) - for callback, eventfilter in listeners: - if not eventfilter or eventfilter in msg: - if msg == 'shutdown': - # the shutdown event should be awaited - await callback(msg, msg_details) - else: - self.event_loop.create_task(callback(msg, msg_details)) - - async def add_event_listener(self, cb, eventfilter=None): - ''' add callback to our event listeners ''' - cb_id = str(uuid.uuid4()) - self.event_listeners[cb_id] = (cb, eventfilter) - return cb_id - - async def remove_event_listener(self, cb_id): - ''' remove callback from our event listeners ''' - self.event_listeners.pop(cb_id, None) - - def run_task(self, corofcn, wait_for_result=False, ignore_exception=None): - ''' helper to run a task on the main event loop from another thread ''' - if threading.current_thread() is threading.main_thread(): - raise Exception("Can not be called from main event loop!") - future = asyncio.run_coroutine_threadsafe(corofcn, self.event_loop) - if wait_for_result: - try: - return future.result() - except Exception as exc: - if ignore_exception and isinstance(exc, ignore_exception): - return None - raise exc - return future +"""Init file for Music Assistant.""" \ No newline at end of file diff --git a/music_assistant/__main__.py b/music_assistant/__main__.py new file mode 100755 index 00000000..1639fb7c --- /dev/null +++ b/music_assistant/__main__.py @@ -0,0 +1,80 @@ +"""Start Music Assistant.""" +import argparse +import platform +import sys +import os +import logging +import asyncio +from aiorun import run + +from music_assistant.mass import MusicAssistant + + +def get_arguments(): + """Arguments handling.""" + parser = argparse.ArgumentParser(description="MusicAssistant") + + data_dir = os.getenv("APPDATA") if os.name == "nt" else os.path.expanduser("~") + data_dir = os.path.join(data_dir, ".musicassistant") + if not os.path.isdir(data_dir): + os.makedirs(data_dir) + + parser.add_argument( + "-c", + "--config", + metavar="path_to_config_dir", + default=data_dir, + help="Directory that contains the MusicAssistant configuration", + ) + parser.add_argument( + "--debug", default=False, help="Start MusicAssistant with verbose debug logging" + ) + arguments = parser.parse_args() + return arguments + + +def main(): + """Start MusicAssistant.""" + # setup logger + logger = logging.getLogger() + logformat = logging.Formatter( + "%(asctime)-15s %(levelname)-5s %(name)s.%(module)s -- %(message)s" + ) + consolehandler = logging.StreamHandler() + consolehandler.setFormatter(logformat) + logger.addHandler(consolehandler) + + # parse arguments + args = get_arguments() + data_dir = args.config + # create event_loop with uvloop + event_loop = asyncio.get_event_loop() + try: + import uvloop + + uvloop.install() + except ImportError: + # uvloop is not available on Windows so safe to ignore this + logger.warning("uvloop support is disabled") + # config debug settings if needed + if args.debug: + event_loop.set_debug(True) + logger.setLevel(logging.DEBUG) + logging.getLogger("aiosqlite").setLevel(logging.INFO) + logging.getLogger("asyncio").setLevel(logging.WARNING) + else: + logger.setLevel(logging.INFO) + + mass = MusicAssistant(data_dir, event_loop) + + # run UI in browser on windows and macos only + if platform.system() in ["Windows", "Darwin"]: + import webbrowser + + webbrowser.open(f"http://localhost:{mass.web.http_port}") + + run(mass.start(), loop=event_loop) + + +if __name__ == "__main__": + main() diff --git a/music_assistant/mass.py b/music_assistant/mass.py new file mode 100644 index 00000000..628faf69 --- /dev/null +++ b/music_assistant/mass.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +import asyncio +import re +import os +import shutil +import slugify as unicode_slug +import uuid +import json +import time +import logging +import threading + +from .database import Database +from .config import MassConfig +from .utils import run_periodic, LOGGER, try_parse_bool, serialize_values +from .metadata import MetaData +from .cache import Cache +from .music_manager import MusicManager +from .player_manager import PlayerManager +from .http_streamer import HTTPStreamer +from .homeassistant import HomeAssistant +from .web import Web + + +class MusicAssistant(): + + def __init__(self, datapath, event_loop): + ''' + Create an instance of MusicAssistant + :param datapath: file location to store the data + :param event_loop: asyncio event_loop + ''' + self.event_loop = event_loop + self.event_loop.set_exception_handler(self.handle_exception) + self.datapath = datapath + self.event_listeners = {} + self.config = MassConfig(self) + # init modules + self.db = Database(self) + self.cache = Cache(self) + self.metadata = MetaData(self) + self.web = Web(self) + self.hass = HomeAssistant(self) + self.music = MusicManager(self) + self.players = PlayerManager(self) + self.http_streamer = HTTPStreamer(self) + + async def start(self): + ''' start running the music assistant server ''' + await self.db.setup() + await self.cache.setup() + await self.metadata.setup() + await self.hass.setup() + await self.music.setup() + await self.players.setup() + await self.web.setup() + await self.http_streamer.setup() + # wait for exit + try: + while True: + await asyncio.sleep(3600) + except asyncio.CancelledError: + LOGGER.info("Application shutdown") + await self.signal_event("shutdown") + self.config.save() + await self.db.close() + await self.cache.close() + + def handle_exception(self, loop, context): + ''' global exception handler ''' + LOGGER.debug(f"Caught exception: {context}") + loop.default_exception_handler(context) + + async def signal_event(self, msg, msg_details=None): + ''' signal (systemwide) event ''' + if not (msg_details == None or isinstance(msg_details, (str, dict))): + msg_details = serialize_values(msg_details) + listeners = list(self.event_listeners.values()) + for callback, eventfilter in listeners: + if not eventfilter or eventfilter in msg: + if msg == 'shutdown': + # the shutdown event should be awaited + await callback(msg, msg_details) + else: + self.event_loop.create_task(callback(msg, msg_details)) + + async def add_event_listener(self, cb, eventfilter=None): + ''' add callback to our event listeners ''' + cb_id = str(uuid.uuid4()) + self.event_listeners[cb_id] = (cb, eventfilter) + return cb_id + + async def remove_event_listener(self, cb_id): + ''' remove callback from our event listeners ''' + self.event_listeners.pop(cb_id, None) + + def run_task(self, corofcn, wait_for_result=False, ignore_exception=None): + ''' helper to run a task on the main event loop from another thread ''' + if threading.current_thread() is threading.main_thread(): + raise Exception("Can not be called from main event loop!") + future = asyncio.run_coroutine_threadsafe(corofcn, self.event_loop) + if wait_for_result: + try: + return future.result() + except Exception as exc: + if ignore_exception and isinstance(exc, ignore_exception): + return None + raise exc + return future diff --git a/requirements.txt b/requirements.txt index 3389454e..efbc9cd7 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ +argparse cytoolz aiohttp[speedups] requests spotify_token protobuf pychromecast -uvloop asyncio_throttle aiocometd aiosqlite @@ -19,4 +19,5 @@ aiorun soco pillow aiohttp_cors -unidecode \ No newline at end of file +unidecode +webbrowser \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..d0377619 --- /dev/null +++ b/setup.py @@ -0,0 +1,34 @@ +# Upload to PyPI Live +# sudo python3 setup.py sdist bdist_wheel +# sudo python3 -m twine upload dist/* + +import setuptools +import os + +VERSION = "0.0.20" +NAME = "music_assistant" + +with open("README.md", "r") as fh: + LONG_DESC = fh.read() + +with open('requirements.txt') as f: + INSTALL_REQUIRES = f.read().splitlines() +if os.name != "nt": + INSTALL_REQUIRES.append("uvloop") + +setuptools.setup( + name=NAME, + version=VERSION, + author='Marcel van der Veldt', + author_email='marcelveldt@users.noreply.github.com', + description='Music library manager and player based on sox.', + long_description=LONG_DESC, + long_description_content_type="text/markdown", + url = 'https://github.com/marcelveldt/musicassistant.git', + packages=['music_assistant'], + classifiers=( + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + ), + install_requires=INSTALL_REQUIRES, + ) \ No newline at end of file