# 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/>.
"""
Overview of all WebAPI Methods sorted by category (some methods appear multiple times).
Available Methods
^^^^^^^^^^^^^^^^^
Artists
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetArtists`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetArtistsWithAlbums` (Alternative: GetFilteredArtistsWithAlbums)
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetFilteredArtistsWithVideos`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.CreateArtistEntry`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.UpdateArtistEntry`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RemoveArtistEntry`
Albums
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetAlbums`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetHiddenAlbums`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSortedAlbumCDs`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetAlbum`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.HideAlbum`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetAlbumOrigin`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetAlbumImportTime`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetAlbumColor`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddAlbumToQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.UpdateAlbumEntry`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RemoveAlbumEntry`
Songs
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSong`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddSongToQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddRandomSongToQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RemoveSongFromQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.MoveSongInQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSongRelationship`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.UpdateSongStatistic`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.CutSongRelationship`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.PlayNextSong`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.CreateSongEntry`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.UpdateSongEntry`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RemoveSongEntry`
Videos
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideos`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideo`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddVideoToQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddRandomVideoToQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RemoveVideoFromQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.MoveVideoInQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.UpdateVideoStatistic`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetVideoColor`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetVideoTimeFrame`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.PlayNextVideo`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.VideoEnded`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetVideoThumbnail`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideoRelationship`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.CutVideoRelationship`
Queue
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSongQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideoQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddSongToQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddRandomSongToQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddVideoToQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddAlbumToQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RemoveSongFromQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RemoveVideoFromQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.MoveSongInQueue`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.MoveVideoInQueue`
Tag related
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetTags`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetTagsStatistics`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSongTags`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetAlbumTags`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideoTags`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetAlbumTag`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RemoveAlbumTag`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetSongTag`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RemoveSongTag`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetVideoTag`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RemoveVideoTag`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddGenre`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddSubgenre`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddMoodFlag`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.DeleteTag`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.ModifyTag`
Lyrics
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSongLyrics`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetSongLyrics`
Uploading
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.InitiateUpload`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.UploadChunk`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetCurrentTasks`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AnnotateUpload`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.InitiateContentIntegration`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.InitiateMusicImport`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.InitiateArtworkImport`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RemoveUpload`
File Handling
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SaveWebUIConfiguration`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.LoadWebUIConfiguration`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.FindNewContent`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.FindAlbumSongFiles`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RenameMusicFile`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RenameAlbumDirectory`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.RenameArtistDirectory`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.ChangeArtistDirectory`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.InitiateFilesystemScan`
Other
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.Find`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetAudioStreamState`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideoStreamState`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetAudioStreamState`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetVideoStreamState`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.PlayNextSong`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.PlayNextVideo`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.SetMDBState`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetMDBState`
* :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetTables`
"""
import random
from musicdb.lib.db.musicdb import *
from musicdb.lib.db.trackerdb import TrackerDatabase
from musicdb.lib.db.musicdb import MusicDatabase
from musicdb.lib.cfg.musicdb import MusicDBConfig
from musicdb.lib.cfg.mdbstate import MDBState
from musicdb.lib.cfg.webui import WebUIConfig
from musicdb.lib.cfg.wsapikey import WebSocketAPIKey
from musicdb.lib.filesystem import Filesystem
from musicdb.lib.metatags import MetaTags
import os
from musicdb.mdbapi.music import MusicDBMusic
from musicdb.mdbapi.musicdirectory import MusicDirectory
from musicdb.mdbapi.mise import MusicDBMicroSearchEngine
from musicdb.mdbapi.tags import MusicDBTags
from musicdb.mdbapi.audiostream import AudioStreamManager
from musicdb.mdbapi.videostream import VideoStreamManager
from musicdb.mdbapi.songqueue import SongQueue
from musicdb.mdbapi.videoqueue import VideoQueue
from musicdb.mdbapi.videoframes import VideoFrames
from musicdb.taskmanagement.taskmanager import TaskManager
from musicdb.taskmanagement.uploadmanager import UploadManager
from musicdb.taskmanagement.integrationmanager import IntegrationManager
from musicdb.taskmanagement.importmanager import ImportManager
from musicdb.taskmanagement.artworkmanager import ArtworkManager
from musicdb.taskmanagement.filesystemmanager import FilesystemManager
import logging
from threading import Thread
import traceback
[docs]class MusicDBWebSocketInterface(object):
def __init__(self):
# Import global variables from the server
from musicdb.mdbapi.server import database, mise, cfg
self.database = database
self.mise = mise
self.cfg = cfg
# The autobahn framework silently hides all exceptions - that sucks
# So all possible exceptions must be caught here, so that they can be made visible.
try:
self.apikey = WebSocketAPIKey(self.cfg).Read()
self.fs = Filesystem(self.cfg.directories.music)
self.tags = MusicDBTags(self.cfg, self.database)
self.mdbstate = MDBState(self.cfg.directories.state, self.database)
self.audiostream= AudioStreamManager(self.cfg, self.database)
self.videostream= VideoStreamManager(self.cfg, self.database)
self.songqueue = SongQueue(self.cfg, self.database)
self.videoqueue = VideoQueue(self.cfg, self.database)
self.music = MusicDBMusic(self.cfg, self.database)
self.musicdirectory = MusicDirectory(self.cfg)
self.taskmanager = TaskManager(self.cfg, self.database)
self.uploadmanager = UploadManager(self.cfg, self.database)
self.integrationmanager = IntegrationManager(self.cfg, self.database)
self.importmanager = ImportManager(self.cfg, self.database)
self.artworkmanager = ArtworkManager(self.cfg, self.database)
self.filesystemmanager = FilesystemManager(self.cfg, self.database)
except Exception as e:
logging.exception(e)
raise e
def onWSConnect(self):
self.audiostream.RegisterCallback(self.onAudioStreamEvent)
self.videostream.RegisterCallback(self.onVideoStreamEvent)
self.songqueue.RegisterCallback(self.onSongQueueEvent)
self.videoqueue.RegisterCallback(self.onVideoQueueEvent)
self.taskmanager.RegisterCallback(self.onTaskEvent)
return None
def onWSDisconnect(self, wasClean, code, reason):
self.audiostream.RemoveCallback(self.onAudioStreamEvent)
self.videostream.RemoveCallback(self.onVideoStreamEvent)
self.songqueue.RemoveCallback(self.onSongQueueEvent)
self.videoqueue.RemoveCallback(self.onVideoQueueEvent)
self.taskmanager.RemoveCallback(self.onTaskEvent)
return None
def onAudioStreamEvent(self, event, data):
# This function is called from a different thread. Therefore NO sqlite3-access is allowed.
# So there will be just a notification so that the clients can request GetAudioStreamState.
response = {}
response["method"] = "notification"
response["fncname"] = "MusicDB:AudioStream"
response["fncsig"] = "on"+event
response["arguments"] = data
response["pass"] = None
success = self.SendPacket(response)
return success
def onVideoStreamEvent(self, event, data):
# This function is called from a different thread. Therefore NO sqlite3-access is allowed.
# So there will be just a notification so that the clients can request GetVideoStreamState.
response = {}
response["method"] = "notification"
response["fncname"] = "MusicDB:VideoStream"
response["fncsig"] = "on"+event
response["arguments"] = data
response["pass"] = None
success = self.SendPacket(response)
return success
def onSongQueueEvent(self, event, data):
# This function is called from a different thread. Therefore NO sqlite3-access is allowed.
# So there will be just a notification so that the clients can request related functions.
response = {}
response["method"] = "notification"
response["fncname"] = "MusicDB:SongQueue"
response["fncsig"] = "on"+event
response["arguments"] = data
response["pass"] = None
success = self.SendPacket(response)
return success
def onVideoQueueEvent(self, event, data):
# This function is called from a different thread. Therefore NO sqlite3-access is allowed.
# So there will be just a notification so that the clients can request related functions.
response = {}
response["method"] = "notification"
response["fncname"] = "MusicDB:VideoQueue"
response["fncsig"] = "on"+event
response["arguments"] = data
response["pass"] = None
success = self.SendPacket(response)
return success
def onTaskEvent(self, notification, data):
# This function is called from a different thread. Therefore NO sqlite3-access is allowed.
# So there will be just a notification so that the clients can request related functions.
# Append uploads to notification except for high frequent ChunkRequest
if notification != "ChunkRequest":
data["tasklist"] = self.GetCurrentTasks()
response = {}
response["method"] = "notification"
response["fncname"] = "MusicDB:Task"
response["fncsig"] = notification
response["arguments"] = data
response["pass"] = None
success = self.SendPacket(response)
return success
def HandleCall(self, fncname, method, fncsig, args, passthrough):
retval = None
# Request-Methods
if fncname == "Bounce":
retval = self.Bounce(args)
elif fncname == "GetArtists":
retval = self.GetArtists()
elif fncname == "GetArtistsWithAlbums":
retval = self.GetArtistsWithAlbums()
elif fncname == "GetFilteredArtistsWithAlbums":
retval = self.GetArtistsWithAlbums(applyfilter=True)
elif fncname == "GetFilteredArtistsWithVideos":
retval = self.GetFilteredArtistsWithVideos()
elif fncname == "GetHiddenAlbums":
retval = self.GetHiddenAlbums()
elif fncname == "GetAlbums":
retval = self.GetAlbums(args["artistid"], args["applyfilter"])
elif fncname == "GetAlbum":
retval = self.GetAlbum(args["albumid"])
elif fncname == "GetVideos":
retval = self.GetVideos(args["artistid"])
elif fncname == "GetVideo":
retval = self.GetVideo(args["videoid"])
elif fncname == "GetSortedAlbumCDs":
retval = self.GetSortedAlbumCDs(args["albumid"])
elif fncname == "GetSong":
retval = self.GetSong(args["songid"])
elif fncname == "GetTags":
retval = self.GetTags()
elif fncname == "GetTagsStatistics":
retval = self.GetTagsStatistics()
elif fncname == "GetSongTags":
retval = self.GetSongTags(args["songid"])
elif fncname == "GetAlbumTags":
retval = self.GetAlbumTags(args["albumid"])
elif fncname == "GetTables":
retval = self.GetTables(args["tables"])
elif fncname == "GetMDBState":
retval = self.GetMDBState()
elif fncname == "GetStreamState":
logging.warning("GetStreamState is deprecated! Use GetAudioStreamState instead. \033[1;30m(Calling GetAudioStreamState)")
retval = self.GetAudioStreamState()
elif fncname == "GetAudioStreamState":
retval = self.GetAudioStreamState()
elif fncname == "GetVideoStreamState":
retval = self.GetVideoStreamState()
elif fncname == "GetSongQueue":
retval = self.GetSongQueue()
elif fncname == "GetVideoQueue":
retval = self.GetVideoQueue()
elif fncname == "Find":
retval = self.Find(args["searchstring"], args["limit"])
elif fncname == "GetSongRelationship":
retval = self.GetSongRelationship(args["songid"])
elif fncname == "GetVideoRelationship":
retval = self.GetVideoRelationship(args["videoid"])
elif fncname == "GetSongLyrics":
retval = self.GetSongLyrics(args["songid"])
elif fncname == "LoadWebUIConfiguration":
retval = self.LoadWebUIConfiguration()
elif fncname == "FindNewContent":
retval = self.FindNewContent()
elif fncname == "FindAlbumSongFiles":
retval = self.FindAlbumSongFiles(args["albumpath"])
elif fncname == "RenameMusicFile":
retval = self.RenameMusicFile(args["oldpath"], args["newpath"])
elif fncname == "RenameAlbumDirectory":
retval = self.RenameAlbumDirectory(args["oldpath"], args["newpath"])
elif fncname == "RenameArtistDirectory":
retval = self.RenameArtistDirectory(args["oldpath"], args["newpath"])
elif fncname == "CreateArtistEntry":
retval = self.CreateArtistEntry(args["name"])
elif fncname == "RemoveArtistEntry":
retval = self.RemoveArtistEntry(args["artistid"])
elif fncname == "UpdateArtistEntry":
retval = self.UpdateArtistEntry(args["artistid"], args["newpath"])
elif fncname == "ChangeArtistDirectory":
retval = self.ChangeArtistDirectory(args["oldalbumpath"], args["newartistdirectory"])
elif fncname == "GetCurrentTasks":
retval = self.GetCurrentTasks()
elif fncname == "AnnotateUpload":
retval = self.AnnotateUpload(args["taskid"], args)
elif fncname == "InitiateContentIntegration":
retval = self.InitiateContentIntegration(args["taskid"], args["musicpath"])
elif fncname == "InitiateMusicImport":
retval = self.InitiateMusicImport(args["contenttype"], args["contentpath"])
elif fncname == "InitiateArtworkImport":
retval = self.InitiateArtworkImport(args["sourcepath"], args["targetpath"])
elif fncname == "InitiateFilesystemScan":
retval = self.InitiateFilesystemScan()
elif fncname == "RemoveUpload":
retval = self.RemoveUpload(args["taskid"])
# Call-Methods (retval will be ignored unless method gets not changed)
elif fncname == "SaveWebUIConfiguration":
retval = self.SaveWebUIConfiguration(args["config"])
elif fncname == "SetMDBState":
retval = self.SetMDBState(args["category"], args["name"], args["value"])
retval = self.GetMDBState()
method = "broadcast"
fncname= "GetMDBState"
elif fncname == "SetSongTag":
if "approval" in args and "confidence" in args:
retval = self.SetSongTag(args["songid"], args["tagid"], args["approval"], args["confidence"])
else:
retval = self.SetSongTag(args["songid"], args["tagid"])
retval = self.GetSong(args["songid"])
method = "broadcast"
fncname= "GetSong"
elif fncname == "RemoveSongTag":
retval = self.RemoveSongTag(args["songid"], args["tagid"])
retval = self.GetSong(args["songid"])
method = "broadcast"
fncname= "GetSong"
elif fncname == "SetVideoTag":
retval = self.SetVideoTag(args["videoid"], args["tagid"])
retval = self.GetVideo(args["videoid"])
method = "broadcast"
fncname= "GetVideo"
elif fncname == "RemoveVideoTag":
retval = self.RemoveVideoTag(args["videoid"], args["tagid"])
retval = self.GetVideo(args["videoid"])
method = "broadcast"
fncname= "GetVideo"
elif fncname == "AddGenre":
retval = self.AddGenre(args["name"])
retval = self.GetTags()
method = "broadcast"
fncname= "GetTags"
elif fncname == "AddSubgenre":
retval = self.AddSubgenre(args["name"], args["parentname"])
retval = self.GetTags()
method = "broadcast"
fncname= "GetTags"
elif fncname == "AddMoodFlag":
retval = self.AddMoodFlag(args["name"], args["icon"], args["color"], args["posx"], args["posy"])
retval = self.GetTags()
method = "broadcast"
fncname= "GetTags"
elif fncname == "DeleteTag":
retval = self.DeleteTag(args["tagid"])
retval = self.GetTags()
method = "broadcast"
fncname= "GetTags"
elif fncname == "ModifyTag":
retval = self.ModifyTag(args["tagid"], args["attribute"], args["value"])
retval = self.GetTags()
method = "broadcast"
fncname= "GetTags"
elif fncname == "SetAlbumTag":
retval = self.SetAlbumTag(args["albumid"], args["tagid"])
retval = self.GetAlbum(args["albumid"])
method = "broadcast"
fncname= "GetAlbum"
elif fncname == "RemoveAlbumTag":
retval = self.RemoveAlbumTag(args["albumid"], args["tagid"])
retval = self.GetAlbum(args["albumid"])
method = "broadcast"
fncname= "GetAlbum"
elif fncname == "SetSongLyrics":
retval = self.SetSongLyrics(args["songid"], args["lyrics"], args["lyricsstate"])
retval = self.GetSong(args["songid"])
method = "broadcast"
fncname= "GetSong"
elif fncname == "HideAlbum":
retval = self.HideAlbum(args["albumid"], args["hide"])
elif fncname == "SetAlbumOrigin":
retval = self.SetAlbumOrigin(args["albumid"], args["origin"])
elif fncname == "SetAlbumImportTime":
retval = self.SetAlbumImportTime(args["albumid"], args["importtime"])
elif fncname == "SetAlbumColor":
retval = self.SetAlbumColor(args["albumid"], args["colorname"], args["color"])
elif fncname == "SetVideoColor":
retval = self.SetVideoColor(args["videoid"], args["colorname"], args["color"])
elif fncname == "SetVideoTimeFrame":
retval = self.SetVideoTimeFrame(args["videoid"], args["begin"], args["end"])
elif fncname == "UpdateSongStatistic":
retval = self.UpdateSongStatistic(args["songid"], args["statistic"], args["modifier"])
retval = self.GetSong(args["songid"])
method = "broadcast"
fncname= "GetSong"
elif fncname == "UpdateVideoStatistic":
retval = self.UpdateVideoStatistic(args["videoid"], args["statistic"], args["modifier"])
retval = self.GetVideo(args["videoid"])
method = "broadcast"
fncname= "GetVideo"
elif fncname == "AddSongToQueue":
retval = self.AddSongToQueue(args["songid"], args["position"])
elif fncname == "AddVideoToQueue":
retval = self.AddVideoToQueue(args["videoid"], args["position"])
elif fncname == "AddRandomVideoToQueue":
retval = self.AddRandomVideoToQueue(args["position"])
elif fncname == "AddAlbumToQueue":
if "cd" in args:
retval = self.AddAlbumToQueue(args["albumid"], args["position"], args["cd"])
else:
retval = self.AddAlbumToQueue(args["albumid"], args["position"])
elif fncname == "AddRandomSongToQueue":
if "albumid" in args:
retval = self.AddRandomSongToQueue(args["position"], args["albumid"])
else:
retval = self.AddRandomSongToQueue(args["position"])
elif fncname == "MoveSongInQueue":
retval = self.MoveSongInQueue(args["entryid"], args["afterid"])
elif fncname == "MoveVideoInQueue":
retval = self.MoveVideoInQueue(args["entryid"], args["afterid"])
elif fncname == "RemoveSongFromQueue":
retval = self.RemoveSongFromQueue(args["entryid"])
elif fncname == "RemoveVideoFromQueue":
retval = self.RemoveVideoFromQueue(args["entryid"])
elif fncname == "RemoveSongEntry":
retval = self.RemoveSongEntry(args["songid"])
elif fncname == "UpdateSongEntry":
retval = self.UpdateSongEntry(args["songid"], args["newpath"])
elif fncname == "CreateSongEntry":
retval = self.CreateSongEntry(args["newpath"])
elif fncname == "RemoveAlbumEntry":
retval = self.RemoveAlbumEntry(args["albumid"])
elif fncname == "UpdateAlbumEntry":
retval = self.UpdateAlbumEntry(args["albumid"], args["newpath"])
elif fncname == "CutSongRelationship":
retval = self.CutSongRelationship(args["songid"], args["relatedsongid"])
if method == "request":
retval = self.GetSongRelationship(args["songid"])
fncname= "GetSongRelationship"
elif fncname == "CutVideoRelationship":
retval = self.CutVideoRelationship(args["videoid"], args["relatedvideoid"])
if method == "request":
retval = self.GetVideoRelationship(args["videoid"])
fncname= "GetVideoRelationship"
elif fncname == "SetStreamState":
logging.warning("SetStreamState is deprecated! Use SetAudioStreamState instead. \033[1;30m(Calling SetAudioStreamState)")
retval = self.SetAudioStreamState(args["state"])
elif fncname == "SetAudioStreamState":
retval = self.SetAudioStreamState(args["state"])
elif fncname == "SetVideoStreamState":
retval = self.SetVideoStreamState(args["state"])
elif fncname == "PlayNextSong":
retval = self.PlayNextSong()
elif fncname == "PlayNextVideo":
retval = self.PlayNextVideo()
elif fncname == "VideoEnded":
retval = self.VideoEnded(args["entryid"])
elif fncname == "SetVideoThumbnail":
retval = self.SetVideoThumbnail(args["videoid"], args["timestamp"])
elif fncname == "InitiateUpload":
retval = self.InitiateUpload(args["taskid"], args["mimetype"], args["contenttype"], args["filesize"], args["checksum"], args["filename"])
elif fncname == "UploadChunk":
retval = self.UploadChunk(args["taskid"], args["chunkdata"])
else:
logging.warning("Unknown function: %s! \033[0;33m(will be ignored)", str(fncname))
return None
# prepare return behaviour
response = {}
response["fncname"] = fncname
response["fncsig"] = fncsig
response["arguments"] = retval
response["pass"] = passthrough
if method == "request":
response["method"] = "response"
self.SendPacket(response)
elif method == "broadcast":
response["method"] = "broadcast"
self.BroadcastPacket(response)
return None
def onCall(self, packet):
try:
method = packet["method"]
fncname = packet["fncname"]
fncsig = packet["fncsig"]
arguments = packet["arguments"]
passthrough = packet["pass"]
apikey = packet["key"]
except:
logging.warning("Malformed request packet received! \033[0;33m(Call will be ignored)")
logging.debug("Packet: %s", str(packet))
return False
logging.debug("method: %s, fncname: \033[1;37m%s\033[1;30m, fncsig: %s, arguments: %.200s, pass: %s",
str(method),str(fncname),str(fncsig),str(arguments),str(passthrough))
if apikey != self.apikey:
logging.error("Invalid WebSocket API Key! \033[1;30m(Check your configuration. If they are correct check your HTTP servers security!)\033[0m\nreceived: %s\nexpected: %s", str(apikey), str(self.apikey))
return False
if not method in ["call", "request", "broadcast"]:
logging.warning("Unknown call-method: %s! \033[0;33m(Call will be ignored)", str(method))
return False
try:
self.HandleCall(fncname, method, fncsig, arguments, passthrough)
except Exception as e:
logging.exception("Unexpected error for async. call-function: %s!", str(fncname))
return False
return True
[docs] def Bounce(self, args):
"""
This is a special method that does nothing but returning the arguments given to it.
It can be used to propagate information between different clients or trigger certain events inside a client.
Args:
args (dict): A dictionary with any data
Returns:
The dictionary given as argument
Example:
.. code-block:: javascript
MusicDB_Broadcast("Bounce", "UpdateValue", {value: 42}); // Tell all clients that value is 42
MusicDB_Request("Bounce", "TriggerEvent"); // Trigger an event inside the client
"""
return args
[docs] def GetArtists(self):
"""
Returns a list of artists.
This list is sorted by the name of the artist.
A list entry contains the following values:
* **id:** Artist ID
* **name:** Artist name
* **path:** Relative path to the artist directory
Returns:
A list of artists
"""
artists = self.database.GetAllArtists() # Get artists from the database
artists = sorted(artists, key = lambda k: k["name"].lower())# Sort the artists for their name
return artists
[docs] def GetArtistsWithAlbums(self, applyfilter=False):
"""
This method returns a list of artists and their albums.
Each entry in this list contains the following two elements:
* **artist:** An entry like the list entries of :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetArtists`
* **albums:** A list of albums that was returned by :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetAlbums` for the related artist.
Artists without albums or albums that got filters out will not appear in the list.
.. attention::
The JavaScript API uses the following aliases:
* ``GetArtistsWithAlbums``: Without arguments, and so without applying the filter.
* ``GetFilteredArtistsWithAlbums``: With applying the filter option.
So, from JavaScripts point of view this method does not have any parameters.
Args:
applyfilter (bool): Default value is ``False``
Returns:
A list of artists and their albums
Example:
.. code-block:: javascript
MusicDB_Request("GetFilteredArtistsWithAlbums", "ShowArtists");
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetFilteredArtistsWithAlbums" && sig == "ShowArtists")
{
for(let artist of args)
{
console.log("Artist: " + artist.name);
for(let albumdata of artist.albums)
console.log(albumdata.album); // album entry
console.log(albumdata.tags); // tags for the album
}
}
}
"""
# Get artist-list
artists = self.GetArtists()
# Get album for each artist
artistlist = []
for artist in artists:
albums = self.GetAlbums(artist["id"], applyfilter)
# filter artists with no relevant albums
if applyfilter and albums == []:
continue
entry = {}
entry["artist"] = artist
entry["albums"] = albums
artistlist.append(entry)
return artistlist
[docs] def GetFilteredArtistsWithVideos(self):
"""
This method returns a list of artists and their videos.
The genre-filter gets applied to the videos.
Each entry in this list contains the following two elements:
* **artist:** An entry like the list entries of :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetArtists`
* **videos:** A list of videos
Artists without videos or videos that got filters out will not appear in the list.
Returns:
A list of artists and their videos
Example:
.. code-block:: javascript
MusicDB_Request("GetFilteredArtistsWithVideos", "ShowArtists");
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetFilteredArtistsWithVideos" && sig == "ShowArtists")
{
for(let artist of args)
{
console.log("Artist: " + artist.name);
for(let video of artist.videos)
console.log(" -> " + video.name);
}
}
}
"""
# Get artist-list
artists = self.GetArtists()
# Get videos for each artist
artistlist = []
for artist in artists:
videos = self.GetVideos(artist["id"], applyfilter=True)
# filter artists with no relevant videos
if videos == []:
continue
entry = {}
entry["artist"] = artist
entry["videos"] = videos
artistlist.append(entry)
return artistlist
[docs] def GetHiddenAlbums(self):
"""
GetHiddenAlbums returns a list of all albums that are flagged as *hidden*.
The list is sorted by artist and release date of the album, starting with the earliest one.
Actually the list is sorted by the albums path.
Because of the naming scheme it leads to push alphabetic artists order and release-date order.
Each entry in the list has the following two elements:
* **album:** An album entry from the database.
* **tags:** The returned tag entry by :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetAlbumTags`
Returns:
A list of albums and their tags
Example:
.. code-block:: javascript
MusicDB_Request("GetHiddenAlbums", "ShowAlbums");
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetHiddenAlbums" && sig == "ShowAlbums")
{
for(let listentry of args)
{
let album, tags;
album = listentry.album;
tags = listentry.tags;
console.log("Tags of " + album.name + ":");
console.log(tags);
}
}
}
"""
# Get albums by this artist
albums = self.database.GetAlbums(hidden="only")
# sort albums for release year
albums = sorted(albums, key = lambda k: k["path"])
# assign tags to albums
albumlist = []
for album in albums:
tags = self.GetAlbumTags(album["id"])
entry = {}
entry["album"] = album
entry["tags"] = tags
albumlist.append(entry)
return albumlist
[docs] def GetAlbums(self, artistid, applyfilter=False):
"""
GetAlbums returns a list of albums of an artist.
The list is sorted by release date of the album, starting with the earliest one.
Each entry in the list has the following two elements:
* **album:** An album entry from the database.
* **tags:** The returned tag entry by :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetAlbumTags`
The filter gets applied to only the *genre* tags.
Args:
artistid (int): ID of the artist whose albums shall be returned
applyfilter (bool): Default value is ``False``
Returns:
A list of albums and their tags
Example:
.. code-block:: javascript
MusicDB_Request("GetAlbums", "ShowAlbums", {artistid:artistid, applyfilter:false});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetAlbums" && sig == "ShowAlbums")
{
for(let listentry of args)
{
let album, tags;
album = listentry.album;
tags = listentry.tags;
console.log("Tags of " + album.name + ":");
console.log(tags);
}
}
}
"""
# Get albums by this artist
albums = self.database.GetAlbumsByArtistId(artistid)
# sort albums for release year
# In case of an invalid import, the release my become None. So assume a release date of 0 in case it is not a valid integer
albums = sorted(albums, key = lambda k: k["release"] if type(k["release"]) is int else 0)
if applyfilter:
filterset = set(self.mdbstate.GetFilterList())
# assign tags to albums
albumlist = []
for album in albums:
tags = self.GetAlbumTags(album["id"])
genres = tags["genres"]
# if no tags are available, show the album!
if applyfilter and genres:
genreset = { genre["name"] for genre in genres }
# do not continue with this album,
# if there is no unionset of genres
if not filterset & genreset:
continue
entry = {}
entry["album"] = album
entry["tags"] = tags
albumlist.append(entry)
return albumlist
[docs] def GetVideos(self, artistid, applyfilter=False):
"""
GetVideos returns a list of videos of an artist.
The list is sorted by release date of the video, starting with the earliest one.
Each entry in the list has the following two elements:
* **video:** A video entry from the database.
* **tags:** The returned tag entry by :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideoTags`
The filter gets applied to only the *genre* tags.
Args:
artistid (int): ID of the artist whose videos shall be returned
applyfilter (bool): Default value is ``False``
Returns:
A list of videos and their tags
Example:
.. code-block:: javascript
MusicDB_Request("GetVideos", "ShowVideos", {artistid:artistid, applyfilter:false});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetVideos" && sig == "ShowVideos")
{
for(let listentry of args)
{
let video, tags;
video = listentry.video;
tags = listentry.tags;
console.log("Tags of " + video.name + ":");
console.log(tags);
}
}
}
"""
# Get videos by this artist
videos = self.database.GetVideosByArtistId(artistid)
# sort videos for release year
videos = sorted(videos, key = lambda k: k["release"])
if applyfilter:
filterset = set(self.mdbstate.GetFilterList())
# assign tags to videos
videolist = []
for video in videos:
tags = self.GetVideoTags(video["id"])
genres = tags["genres"]
# if no tags are available, show the album!
if applyfilter and genres:
genreset = { genre["name"] for genre in genres }
# do not continue with this album,
# if there is no unionset of genres
if not filterset & genreset:
continue
entry = {}
entry["video"] = video
entry["tags"] = tags
videolist.append(entry)
return videolist
[docs] def GetVideo(self, videoid):
"""
This method returns a video entry from the Music Database.
GetVideo returns a dictionary with the following keys:
* **artist:** A Database entry of the artist of the video
* **album:** The database entry of the album of the video, if known. Can be ``None``.
* **song:** The database entry of the song related to the video, if known. Can be ``None``.
* **video:** The video with the ``videoid`` of the request
* **tags:** A list of tags as returned by :meth:`~GetVideoTags`
Args:
videoid (int): The ID of that video that shall be returned
Returns:
A dictionary with information of the requested video
Example:
.. code-block:: javascript
MusicDB_Request("GetVideo", "ShowVideo", {videoid:1000});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetVideo" && sig == "ShowVideo")
{
console.log("Artist: " + args.artist.name);
console.log("Video: " + args.video.name);
}
}
"""
video = self.database.GetVideoById(videoid)
artist = self.database.GetArtistById(video["artistid"])
tags = self.GetVideoTags(videoid)
if video["albumid"]:
album = self.database.GetAlbumById(video["albumid"])
else:
album = None
if video["songid"]:
song = self.database.GetSongById(video["songid"])
else:
song = None
# send the data to the client
retval = {}
retval["artist"] = artist
retval["album"] = album
retval["song"] = song
retval["video"] = video
retval["tags"] = tags
return retval
[docs] def UpdateVideoStatistic(self, videoid, statistic, modifier):
"""
This video allows setting some statistics and properties for a video.
When this method got called direct via the JavaScript API, the MusicDB server broadcasts the result of :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideo`. (``method = "broadcast", fncname = "GetSong"``)
So each client gets informed about the changes made and can synchronize itself.
This method is a direct interface to :meth:`lib.db.musicdb.MusicDatabase.UpdateVideoStatistic`. See the related documentation for more details.
"""
if self.cfg.debug.disablestats:
logging.info("Updating video statistics disabled. \033[1;33m!!")
return None
try:
self.database.UpdateVideoStatistic(videoid, statistic, modifier)
except ValueError as e:
logging.warning("Updating video statistics failed with error: %s!", str(e))
except Exception as e:
logging.exception("Updating video statistics failed with error: %s!", str(e))
return None
[docs] def GetSortedAlbumCDs(self, albumid):
"""
This method returns a sorted list of CDs of an album.
Each CD entry is a further sorted list of songs.
Each song-entry is a dictionary with the following keys:
* **song:** The database entry of the song
* **tags:** a list of tags as returned by :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebsScketInterface.GetSongTags`
Args:
albumid (int): ID of the album that CD and songs shall be returned
Returns:
A sorted list of CDs, its songs and their tags
"""
# Get the data
album = self.database.GetAlbumById(albumid)
songs = self.database.GetSongsByAlbumId(albumid)
if not album or not songs:
logging.warning("No or empty album behind ID %s!", str(albumid))
return []
# annotate all songs with additional infos like genre-tags
songlist = []
for song in songs:
tags = self.GetSongTags(song["id"])
songentry = {}
songentry["song"] = song
songentry["tags"] = tags
songlist.append(songentry)
# sort the albums songs - if there is no CD, skip for name
sortedcds = []
if album["numofcds"] != 0:
# 1.: separate into CDs
unsortedcds = []
for i in range(0, album["numofcds"]):
cdnum = i + 1 # index 0 is cd 1
cd = [songentry for songentry in songlist if songentry["song"]["cd"] == cdnum]
unsortedcds.append(cd)
# 2.: sort songs of each CD for tracknumber
sortedcds = []
for cd in unsortedcds:
cd = sorted(cd, key = lambda k: k["song"]["number"])
sortedcds.append(cd)
else:
sortedsongs = sorted(songlist, key = lambda k: k["song"]["name"])
sortedcds.append(sortedsongs) # make a "virtual" cd
return sortedcds
[docs] def GetAlbum(self, albumid):
"""
This method returns an album and its songs, separated by CDs.
GetAlbum returns a dictionary with the following keys:
* **artist:** database entry of the artist of the album
* **album:** the database entry of the album itself
* **tags:** a list of tags as returned by :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetAlbumTags`
* **cds:** a list of CDs that contains a list of songs for each CD. See :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSortedAlbumCDs` for details.
Args:
albumid (int): The ID of that album that shall be returned
Returns:
A dictionary with information of the requested album, its tags and songs
Example:
.. code-block:: javascript
MusicDB_Request("GetAlbum", "ShowAlbum", {albumid:albumid});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetAlbum" && sig == "ShowAlbum")
{
console.log("Artist: " + args.artist.name);
console.log("Album: " + args.album.name);
for(let cd of args.cds)
{
for(let track of cd)
{
let song, songtags
song = track.song;
songtags = track.tags;
console.log(" -> " + song.name);
}
}
}
}
"""
album = self.database.GetAlbumById(albumid)
artist = self.database.GetArtistById(album["artistid"])
tags = self.GetAlbumTags(albumid)
sortedcds = self.GetSortedAlbumCDs(albumid)
# send the data to the client
retval = {}
retval["artist"] = artist
retval["album"] = album
retval["tags"] = tags
retval["cds"] = sortedcds
return retval
[docs] def GetSong(self, songid):
"""
This method returns the information of a song, its album and artist, and its tags.
The returned dictionary has the following keys:
* **song:** the database entry of the song itself.
* **artist:** database entry of the artist of the song
* **album:** the database entry of the album in that the song is listed
* **tags:** a list of tags as returned by :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSongTags`
Args:
songid (int): ID of the song
Returns:
A dictionary with information of the song
Example:
.. code-block:: javascript
MusicDB_Request("GetSong", "ShowSong", {songid:songid});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetSong" && sig == "ShowSong")
{
console.log("Artist: " + args.artist.name);
console.log("Album: " + args.album.name);
console.log("Song: " + args.song.name);
for(let mood of args.tags.moods)
{
console.log("Mood: " + mood.name);
}
}
}
"""
song = self.database.GetSongById(songid)
album = self.database.GetAlbumById(song["albumid"])
artist = self.database.GetArtistById(song["artistid"])
tags = self.GetSongTags(songid)
retval = {}
retval["song"] = song
retval["artist"] = artist
retval["album"] = album
retval["tags"] = tags
return retval
[docs] def GetTables(self, tablenames):
"""
Returns a dictionary that contains for each requested table a key.
Behind this key is a list of dictionaries representing the rows of the requested table.
If ``tablenames`` is ``None``, the result is an empty dictionary.
The following list shows all valid names and links to the database calls that returned lists will be returned by this method.:
* ``"songs"``: :meth:`musicdb.lib.db.musicdb.MusicDatabase.GetAllSongs`
* ``"albums"``: :meth:`musicdb.lib.db.musicdb.MusicDatabase.GetAllAlbums`
* ``"artists"``: :meth:`musicdb.lib.db.musicdb.MusicDatabase.GetAllArtists`
* ``"tags"``: :meth:`musicdb.lib.db.musicdb.MusicDatabase.GetAllTags`
Args:
tablenames (list of strings): A list of table names.
Returns:
A dict of lists of database entries
Example:
.. code-block:: javascript
MusicDB_Request("GetTables", "ShowTables", {position:["songs", "albums"]});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetTables" && sig == "ShowTables")
{
console.log("Tags of the song with the ID " + args.songid);
for(let song of args.songs)
console.log(song.name);
for(let album of args.albumss)
console.log(album.name);
}
}
"""
retval = {}
if "songs" in tablenames:
retval["songs"] = self.database.GetAllSongs()
if "albums" in tablenames:
retval["albums"] = self.database.GetAllAlbums()
if "artists" in tablenames:
retval["artists"] = self.database.GetAllArtists()
if "tags" in tablenames:
retval["tags"] = self.database.GetAllTags()
return retval
[docs] def SetMDBState(self, category, name, value):
"""
This method sets the global state of MDB clients
If the state is not available in the global state settings, it will be created.
When the *category* is ``"albumfilter"``,
then *name* must be a Genre-Name and *value* is ``True`` or ``False``.
If a genre gets set to true, all albums for that genre are included in the list of returned albums by methods that use the filter.
After executing this method, the MusicDB server broadcasts the result of :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetMDBState`. (``method = "broadcast", fncname = "GetMDBState"``)
So each client gets informed about the new state.
The category must be ``"albumfilter"`` or ``"MusicDB"`` with name ``"uimode"``!
To set the album filter, :meth:`~musicdb.lib.cfg.config.Config.Set` is used.
For setting the UI mode, :meth:`~musicdb.lib.cfg.mdbstate.MDBState.SetUIMode` is called. Details of valid modes are listed there as well.
Args:
category (str): Category of the state
name (str): Name of the sate
value: Value of the state
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("SetMDBState", {category:"albumfilter", name:"Metal", value:true});
"""
if category == "albumfilter":
try:
self.mdbstate.UpdateFilterList(name, value)
except Exception as e:
logging.warning("Setting Album Filter failed with errror \"%s\"", str(e))
elif category == "MusicDB" and name == "uimode":
try:
self.mdbstate.SetUIMode(value)
except Exception as e:
logging.warning("Setting MusicDB UI Mode failed with errror \"%s\"", str(e))
return None
[docs] def GetMDBState(self):
"""
This method returns the current global state of the MusicDB WebUIs
Currently, the only state existing is the list of selected genres.
The state is a dictionary with the following information:
* **albumfilter:** a list of tag-names of class Genre
* **MusicDB:**
* *uimode*: Defines if UI is in ``"audio"`` or ``"video"`` mode
* **audiostream**
* **isconnected:** ``True`` if MusicDB is connected to Icecast, otherwise ``False``
* **isplaying:** ``True`` if the Streaming Thread is in *playing*-mode, otherwise ``False``
* **currentsong:** The song entry from the database for the song that is currently playing or ``None``
* **videostream**
* **isstreaming:** ``True`` if MusicDB manages the video stream
* **isplaying:** ``True`` if the Streaming Thread is in *playing*-mode, otherwise ``False``
* **currentvideo:** The video entry from the database for the video that is currently playing or ``None``
Returns:
Current global MusicDB WebUI state
Example:
.. code-block:: javascript
MusicDB_Request("GetMDBState", "ShowMDBState");
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetMDBState" && sig == "ShowMDBState")
{
console.log(args.MusicDB.uimode);
let activetags;
activetags = args.albumfilter;
for(let genrename of activetags)
console.log(genrename);
}
}
"""
albumfilter = self.mdbstate.GetFilterList()
# Set some information about the audio stream state
audiostreamstate = self.audiostream.GetStreamState()
audioqueueentry = self.songqueue.CurrentSong()
if audioqueueentry:
currentsongid = audioqueueentry["songid"]
currentsong = self.database.GetSongById(currentsongid)
else:
currentsong = None
# Set some information about the video stream state
videostreamstate = self.videostream.GetStreamState()
videoqueueentry = self.videoqueue.CurrentVideo()
if videoqueueentry:
currentvideoid = videoqueueentry["videoid"]
currentvideo = self.database.GetVideoById(currentvideoid);
else:
currentvideo = None
# put everything together
state = {}
state["albumfilter"] = albumfilter
state["MusicDB"] = {}
state["MusicDB"]["uimode"] = self.mdbstate.GetUIMode()
state["audiostream"] = {};
state["audiostream"]["isconnected"] = audiostreamstate["isconnected"]
state["audiostream"]["isplaying"] = audiostreamstate["isplaying"]
state["audiostream"]["currentsong"] = currentsong
state["videostream"] = {};
state["videostream"]["isstreaming"] = videostreamstate["isstreaming"]
state["videostream"]["isplaying"] = videostreamstate["isplaying"]
state["videostream"]["currentvideo"]= currentvideo
return state
[docs] def LoadWebUIConfiguration(self):
"""
This method loads the configuration for the WebUI from the MusicDB data directory.
The configurations are described at :mod:`~musicdb.lib.cfg.webui`
Example:
.. code-block:: javascript
MusicDB_Request("LoadWebUIConfiguration", "ShowConfig");
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "LoadWebUIConfiguration" && sig == "ShowConfig")
{
console.log("Video mode is " + args.WebUI.videomode); // "enabled" or "disabled"
console.log("Lyrics are " + args.WebUI.lyrics); // "enabled" or "disabled"
}
}
Returns:
A dictionary with all configurations
"""
webuicfg = WebUIConfig(self.cfg.files.webuiconfig)
return webuicfg.LoadConfig()
[docs] def SaveWebUIConfiguration(self, config):
"""
This method saves the whole configuration back into the MusicDB data directory.
The argument to this method must be a dict with the whole configuration as returned by :meth:`~LoadWebUIConfiguration`
The configurations are described at :mod:`~musicdb.lib.cfg.webui`
This function should be called using the ``MusicDB_Broadcast`` method to allow propagating the changes
to other connected clients.
When using the request method the new configuration gets returned similar to :meth:`~LoadWebUIConfiguration`.
Example:
.. code-block:: javascript
webuiconfig.WebUI.videomode = "enabled";
MusicDB_Broadcast("SaveWebUIConfiguration", "UpdateConfig", {config: webuiconfig});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "SaveWebUIConfiguration" && sig == "UpdateConfig")
{
console.log("Someone changed the configuration:");
console.log("Video mode is " + args.WebUI.videomode); // "enabled" or "disabled"
console.log("Lyrics are " + args.WebUI.lyrics); // "enabled" or "disabled"
}
}
Returns:
A dictionary with all configurations
"""
webuicfg = WebUIConfig(self.cfg.files.webuiconfig)
webuicfg.SaveConfig(config)
return webuicfg.LoadConfig()
[docs] def GetAudioStreamState(self):
"""
This method returns the state of the Streaming Thread. (See :doc:`/mdbapi/audiostream`)
The state is a dictionary that has always the following information:
* **isconnected:** ``True`` if MusicDB is connected to Icecast, otherwise ``False``
* **isplaying:** ``True`` if the Streaming Thread is in *playing*-mode, otherwise ``False``
* **hasqueue:** ``True`` when there is at least one song in the queue. When ``False``, the following song information are *not* included!
In case there is a at least one song in the queue, this current streamed song gets returned with the following information:
* **song:** The song entry from the database for the song that is currently playing
* **album:** The related album entry from the database
* **artist:** The related artist entry from the database
* **songtags:** a list of tags as returned by :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSongTags`
* **albumtags:** a list of tags as returned by :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetAlbumTags`
Returns:
The current state of the streaming thread
Example:
.. code-block:: javascript
MusicDB_Request("GetAudioStreamState", "ShowStreamState");
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetAudioStreamState" && sig == "ShowStreamtate")
{
if(args.isconnected == true)
{
console.log("Connection to Icecast established");
}
if(args.hasqueue == true)
{
console.log("Current playing song: " + args.song.name);
}
}
}
"""
state = {}
streamstate = self.audiostream.GetStreamState()
queueentry = self.songqueue.CurrentSong()
if queueentry:
songid = queueentry["songid"]
else:
songid = None
state["isconnected"] = streamstate["isconnected"]
state["isplaying"] = streamstate["isplaying"]
# if no file is given, the queue is empty - or "there is no queue"
if songid:
song = self.database.GetSongById(songid)
album = self.database.GetAlbumById(song["albumid"])
artist = self.database.GetArtistById(song["artistid"])
songtags = self.GetSongTags(song["id"])
albumtags = self.GetAlbumTags(album["id"])
state["song"] = song
state["album"] = album
state["artist"] = artist
state["songtags"] = songtags
state["albumtags"] = albumtags
state["hasqueue"] = True
else:
state["hasqueue"] = False
return state
[docs] def GetVideoStreamState(self):
"""
This method returns the state of the Video Streaming Thread. (See :doc:`/mdbapi/videostream`)
The state is a dictionary that has always the following information:
* **isstreaming:** ``True`` if MusicDB manages the video stream
* **isplaying:** ``True`` if the Streaming Thread is in *playing*-mode, otherwise ``False``
* **hasqueue:** ``True`` when there is at least one song in the queue. When ``False``, the following song information are *not* included!
* **currententry:** (string) UUID of the current entry in the queue - the video that gets streamed, or ``null``.
In case there is a at least one song in the queue, this current streamed song gets returned with the following information:
* **video:** The video entry from the database for the video that is currently playing
* **artist:** The artist entry from the database related to the video
* **videotags:** a list of tags as returned by :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideoTags`
Returns:
The current state of the streaming thread
Example:
.. code-block:: javascript
MusicDB_Request("GetVideoStreamState", "ShowStreamState");
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetVideoStreamState" && sig == "ShowStreamtate")
{
if(args.hasqueue == true)
{
console.log("Current playing video: " + args.video.name);
}
}
}
"""
state = {}
streamstate = self.videostream.GetStreamState()
queueentry = self.videoqueue.CurrentVideo()
if queueentry:
videoid = queueentry["videoid"]
else:
videoid = None
state["isstreaming"] = streamstate["isstreaming"]
state["isplaying"] = streamstate["isplaying"]
if streamstate["currententry"]:
state["currententry"] = str(streamstate["currententry"])
else:
state["currententry"] = None
# if no file is given, the queue is empty - or "there is no queue"
if videoid:
video = self.database.GetVideoById(videoid)
artist = self.database.GetArtistById(video["artistid"])
videotags = self.GetVideoTags(video["id"])
state["video"] = video
state["artist"] = artist
state["videotags"] = videotags
state["hasqueue"] = True
else:
state["hasqueue"] = False
return state
[docs] def GetSongQueue(self):
"""
This method returns a list of songs, albums and artists for each song in the song queue.
If there are no songs in the queue, an empty list gets returned.
Each entry of the list contains the following information:
* **entryid:** A unique ID to identify the entry in the queue (as string because it is a 128 integer that blows JavaScripts mind)
* **israndom:** A boolean value set to ``true`` when the song got added randomly and not explicitly by the user
* **song:** The song entry from the database
* **album:** The related album entry from the database
* **artist:** The related artist entry from the database
Returns:
A list of song, album and artist information for each song in the song queue
Example:
.. code-block:: javascript
MusicDB_Request("GetSongQueue", "ShowSongQueue");
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetSongQueue" && sig == "ShowSongQueue")
{
for(let entry of args)
{
console.log(entry.song.name + " by " + entry.artist.name);
}
}
}
"""
entries = self.songqueue.GetQueue()
# return empty list if there is no queue
if not entries:
return []
queue = []
for queueentry in entries:
song = self.database.GetSongById(queueentry["songid"])
album = self.database.GetAlbumById(song["albumid"])
artist = self.database.GetArtistById(song["artistid"])
entry = {}
entry["entryid"] = str(queueentry["entryid"])
entry["israndom"] = bool(queueentry["israndom"])
entry["song"] = song
entry["album"] = album
entry["artist"] = artist
queue.append(entry)
return queue
[docs] def GetVideoQueue(self):
"""
This method returns a list of videos, albums and artists for each video in the video queue.
If there are no videos in the queue, an empty list gets returned.
The album or artist can be ``null`` if there is no associated to the video.
Each entry of the list contains the following information:
* **entryid:** A unique ID to identify the entry in the queue (as string because it is a 128 integer that blows JavaScripts mind)
* **israndom:** A boolean value set to ``true`` when the video got added randomly and not explicitly by the user
* **video:** The video entry from the database
* **album:** The related album entry from the database or ``null``.
* **artist:** The related artist entry from the database or ``null``.
Returns:
A list of video, album and artist information for each video in the video queue
Example:
.. code-block:: javascript
MusicDB_Request("GetVideoQueue", "ShowVideoQueue");
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetVideoQueue" && sig == "ShowVideoQueue")
{
for(let entry of args)
{
console.log(entry.video.name + " by " + entry.artist.name);
}
}
}
"""
entries = self.videoqueue.GetQueue()
# return empty list if there is no queue
if not entries:
return []
queue = []
for queueentry in entries:
video = self.database.GetVideoById(queueentry["videoid"])
if video["albumid"] != None:
album = self.database.GetAlbumById(video["albumid"])
else:
album = None
if video["artistid"] != None:
artist = self.database.GetArtistById(video["artistid"])
else:
artist = None
entry = {}
entry["entryid"] = str(queueentry["entryid"])
entry["israndom"] = bool(queueentry["israndom"])
entry["video"] = video
entry["album"] = album
entry["artist"] = artist
queue.append(entry)
return queue
[docs] def Find(self, searchstring, limit):
"""
This method starts a search for *searchstring* on songnames, albumnames and artistnames.
The returned value is a dictionary with the following entries:
* **songs:** A list of songs, each entry is a dictionary with the following information:
* **song:** The song entry from the database
* **album:** The related album entry from the database
* **artist:** The related artist entry from the database
* **albums:** A list of albums, each entry is a dictionary with the following information:
* **album:** The related album entry from the database
* **artist:** The related artist entry from the database
* **artists:** A list of artists, each entry is a dictionary with the following information:
* **artist:** The related artist entry from the database
* **albums:** A list of all albums by this artist, ordered by release year
The search does not look for exact matching.
It looks for most likeliness.
So, typos or all lowercase input are no problem.
The list of songs will not contain any hated or disabled songs.
Args:
searchstring (str): The string to search for
limit (int): max number entries that will be returned for each category
Returns:
A dictionary with the search-results
Example:
.. code-block:: javascript
MusicDB_Request("Find", "ShowResults", {searchstring:"cake", limit:5});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "Find" && sig == "ShowResults")
{
for(let entry of args.songs)
console.log(entry.name);
for(let entry of args.albums)
console.log(entry.name);
for(let entry of args.artists)
console.log(entry.name);
}
}
"""
(foundartists, foundalbums, foundsongs) = self.mise.Find(searchstring)
# process found artists
artists = []
artistcount = min(limit, len(foundartists))
for i in range(artistcount):
artistid = foundartists[i][0]
artist = self.database.GetArtistById(artistid)
# Get albums by this artist and sort for release year
albums = self.database.GetAlbumsByArtistId(artistid)
albums = sorted(albums, key = lambda k: k["release"])
entry = {}
entry["artist"] = artist
entry["albums"] = albums
artists.append(entry)
# process found albums
albums = []
albumcount = min(limit, len(foundalbums))
for i in range(albumcount):
albumid = foundalbums[i][0]
album = self.database.GetAlbumById(albumid)
artist = self.database.GetArtistById(album["artistid"])
entry = {}
entry["artist"] = artist
entry["album"] = album
albums.append(entry)
# process found songs
songs = []
songcount = len(foundsongs) # Do not limit the songlist length at this point. There may be songs removed
for i in range(songcount):
songid = foundsongs[i][0]
song = self.database.GetSongById(songid)
if song["disabled"] or song["favorite"] == -1:
continue # Skip hated and disabled songs
album = self.database.GetAlbumById(song["albumid"])
artist = self.database.GetArtistById(song["artistid"])
entry = {}
entry["artist"] = artist
entry["album"] = album
entry["song"] = song
songs.append(entry)
if len(songs) >= limit:
break
results = {}
results["artists"] = artists
results["albums"] = albums
results["songs"] = songs
return results
[docs] def SetAudioStreamState(self, state):
"""
This method can be used to set the *playing*-state of the audio stream (see :doc:`/mdbapi/audiostream`)
The following arguments are possible:
* ``"play"``: Set state to *playing*. If there are songs in the queue, MusicDB starts streaming.
* ``"pause"``: Set state to *pause*.
* ``"playpause"``: Toggle between *playing* and *pause*
Args:
state (str): New playing-state for the audio streaming thread. *state* must be one of the following strings: ``"playpause"``, ``"play"`` or ``"pause"``.
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("SetAudioStreamState", {state:"playpause"});
"""
currentstate = self.audiostream.GetStreamState()
isplaying = currentstate["isplaying"]
if state == "playpause":
if isplaying:
self.audiostream.Play(False)
else:
self.audiostream.Play(True)
elif state == "pause":
self.audiostream.Play(False)
elif state == "play":
self.audiostream.Play(True)
else:
logging.warning("Unexpected state \"%s\" will not be set! \033[1;30m(State must be play, pause or playpause)", str(state))
return None
[docs] def SetVideoStreamState(self, state):
"""
This method can be used to set the *streaming*-state of the video stream (see :doc:`/mdbapi/videostream`)
The following arguments are possible:
* ``"play"``: Set state to *playing*. If there are songs in the queue, MusicDB starts streaming.
* ``"pause"``: Set state to *pause*.
* ``"playpause"``: Toggle between *playing* and *pause*
Args:
state (str): New streaming-state for the video streaming thread. *state* must be one of the following strings: ``"playpause"``, ``"play"`` or ``"pause"``.
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("SetVideoStreamState", {state:"playpause"});
"""
currentstate = self.videostream.GetStreamState()
isplaying = currentstate["isstreaming"]
if state == "playpause":
if isplaying:
self.videostream.Play(False)
else:
self.videostream.Play(True)
elif state == "pause":
self.videostream.Play(False)
elif state == "play":
self.videostream.Play(True)
else:
logging.warning("Unexpected state \"%s\" will not be set! \033[1;30m(State must be play, pause or playpause)", str(state))
return None
[docs] def PlayNextSong(self):
"""
This method skips the current playing song.
If there is no song that can be skipped, the Song Queue or Streaming Thread will handle this properly.
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("PlayNextSong");
"""
self.audiostream.PlayNextSong()
return None
[docs] def PlayNextVideo(self):
"""
This method skips the current playing video.
If there is no video that can be skipped, the Video Queue or Streaming Thread will handle this properly.
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("PlayNextVideo");
"""
self.videostream.PlayNextVideo()
return None
[docs] def VideoEnded(self, entryid):
"""
Notify the Video Queue that the current played video with a specific entry id ended.
In case multiple clients are calling this method, the `~mdbapi.videostream.VideoStreamManager` will handle the conflict.
Args:
entryid (str): UUID of the video entry in the song queue
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("VideoEnded", {entryid:'168493523207840521806064336792025247758'});
"""
try:
entryid = int(entryid)
except ValueError:
logging.debug("Invalid argument. Cannot cast UUID \"%s\" to integer.", str(entryid))
return None
self.videostream.VideoEnded(entryid)
return None
[docs] def SetVideoThumbnail(self, videoid, timestamp):
"""
This method sets a new video thumbnail via :meth:`~musicdb.mdbapi.videoframes.VideoFrames.ChangeThumbnail`.
Args:
videoid (int): ID of the video to update
timestamp (int): Time stamp of the frame to select (in seconds)
Returns:
``None``
Example:
.. code-block:: javascript
// Use frame at 1:10 as thumbnail
MusicDB_Call("SetVideoThumbnail", {videoid:1000, timestamp:70});
"""
video = self.database.GetVideoById(videoid);
if not video:
logging.warning("Invalid video ID: %s! \033[1;30m(ignoring SetVideoThumbnail command)", str(videoid))
return None
videoframes = VideoFrames(self.cfg, self.database)
videoframes.ChangeThumbnail(video, timestamp)
return None
[docs] def AddSongToQueue(self, songid, position):
"""
This method adds a new song to the queue of songs that will be streamed.
The song gets address by its ID.
The position can be ``"next"`` if the song shall be places behind the current playing song.
So, the new added song will be played next.
Alternative ``"last"`` can be used to place the song at the end of the queue.
In case position is an integer, it is interpreted as an entry ID of the SongQueue.
Then the song gets append to that entry.
Args:
songid (int): ID of the song that shall be added
position (str/int): ``"next"``, ``"last"`` or Song-Queue Entry ID - Determines where the song gets added
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("AddSongToQueue", {songid:1000, position:"next"});
"""
# Check if the song ID is valid
song = self.database.GetSongById(songid)
if not song:
logging.warning("Invalid song ID: %s! \033[1;30m(ignoring AddSongToQueue command)", str(songid))
return None
try:
position = int(position)
except:
pass
if type(position) == str and position not in ["next", "last"]:
logging.warning("Position must have the value \"next\" or \"last\" or an integer. Given was \"%s\". \033[1;30m(Doing nothing)", str(position))
return None
# Add song to the queue and update statistics
self.songqueue.AddSong(songid, position)
return None
[docs] def AddRandomSongToQueue(self, position, albumid=None):
"""
Similar to :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddSongToQueue`.
Instead of a specific song, a random song gets chosen by the random-song manager *Randy* (See :doc:`/mdbapi/randy`).
This is done using the :meth:`mdbapi.randy.Randy.GetSong` method.
If an album ID is given, the new song will be added from that album
using :meth:`musicdb.mdbapi.randy.Randy.GetSongFromAlbum`.
Args:
position (str): ``"next"`` or ``"last"`` - Determines where the song gets added
albumid (int): Optional album ID from that the song shall be
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("AddRandomSongToQueue", {position:"last"});
MusicDB_Call("AddRandomSongToQueue", {albumid:"42", position:"next"});
"""
if position not in ["next", "last"]:
logging.warning("Position must have the value \"next\" or \"last\". Given was \"%s\". \033[1;30m(Doing nothing)", str(position))
return None
if albumid != None:
albumid = int(albumid)
self.songqueue.AddRandomSong(position, albumid)
return None
[docs] def AddVideoToQueue(self, videoid, position):
"""
This method adds a new video to the queue of videos that will be played.
The video gets address by its ID.
The position can be ``"next"`` if the video shall be places behind the current playing video.
So, the new added video will be played next.
Alternative ``"last"`` can be used to place the video at the end of the queue.
In case position is an integer, it is interpreted as an entry ID of the VideoQueue.
Then the video gets append to that entry.
Args:
videoid (int): ID of the video that shall be added
position (str/int): ``"next"``, ``"last"`` or Video-Queue Entry ID - Determines where the song gets added
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("AddVideoToQueue", {videoid:1000, position:"last"});
"""
# Check if the video ID is valid
video = self.database.GetVideoById(videoid)
if not video:
logging.warning("Invalid video ID: %s! \033[1;30m(ignoring AddVideoToQueue command)", str(videoid))
return None
try:
position = int(position)
except:
pass
if type(position) == str and position not in ["next", "last"]:
logging.warning("Position must have the value \"next\" or \"last\" or an integer. Given was \"%s\". \033[1;30m(Doing nothing)", str(position))
return None
# Add video to the queue and update statistics
self.videoqueue.AddVideo(videoid, position)
return None
[docs] def AddRandomVideoToQueue(self, position):
"""
Similar to :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.AddVideoToQueue`.
Instead of a specific video, a random video gets chosen by the random music manager *Randy* (See :doc:`/mdbapi/randy`).
This is done using the :meth:`musicdb.mdbapi.randy.Randy.GetVideo` method.
Args:
position (str): ``"next"`` or ``"last"`` - Determines where the song gets added
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("AddRandomVideoToQueue", {position:"last"});
"""
if position not in ["next", "last"]:
logging.warning("Position must have the value \"next\" or \"last\". Given was \"%s\". \033[1;30m(Doing nothing)", str(position))
return None
self.videoqueue.AddRandomVideo(position)
return None
[docs] def AddAlbumToQueue(self, albumid, position, cd=None):
"""
This method adds all songs of an album at any position of the queue.
If *cd* is ``None`` (or not given), all songs of all CDs are added to the queue.
If *cd* is an integer, it limits the CD that will be added.
The CD number starts with ``0`` for the first CD of an album.
The position can be ``"next"`` if the songs shall be placed behind the current playing song.
So, all new added songs will be played next.
Alternative ``"last"`` can be used to place the songs at the end of the queue.
In case position is an integer, it is interpreted as an entry ID of the SongQueue.
Then the songs get append to that addressed entry.
If a song is flagged as "hated" or disabled, than it gets discarded.
Args:
albumid (int): ID of the album that shall be added
position (str/int): ``"next"``, ``"last"`` or Song-Queue Entry ID - Determines where the songs get added
cd (None/int): (Optional) Only this CD of the album should be added to the queue. Starting with ``0`` for the first CD of an album.
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("AddAlbumToQueue", {albumid:23});
MusicDB_Call("AddAlbumToQueue", {albumid:42, position:"next"});
MusicDB_Call("AddAlbumToQueue", {albumid:13, position:"last", cd=1});
"""
try:
position = int(position)
except:
pass
if type(position) == str and position not in ["next", "last"]:
logging.warning("Position must have the value \"next\" or \"last\" or an integer. Given was \"%s\". \033[1;30m(Doing nothing)", str(position))
return None
sortedcds = self.GetSortedAlbumCDs(albumid)
# If there shall be only a specific CD added, limit sortedcds to that specific one
if type(cd) == int:
if cd >= len(sortedcds):
logging.warning("CD index to select is larger than available CDs. Given was CD \"%s\", available are \"%i\" CDs. \033[1;30m(Doing nothing)", str(cd), len(sortedcds))
return None
sortedcds = [sortedcds[cd], ]
# Insert all CDs song-vise into to queue
for cd in sortedcds:
for entry in cd:
song = entry["song"]
if song["disabled"] == 1 or song["favorite"] == -1:
continue
position = self.songqueue.AddSong(song["id"], position)
return None
[docs] def RemoveSongFromQueue(self, entryid):
"""
This method removes a song from song queue.
The song gets identified by the entry ID of the queue entry.
If the song is currently streamed, playing the next song gets triggered.
Args:
entryid (str) Queue entry ID of the song
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("RemoveSongFromQueue", {entryid:"82390194629402649"});
"""
if type(entryid) != str:
logging.warning("entryid must be of type string! Actual type was %s. \033[1;30m(RemoveSongFromQueue will be ignored)", str(type(entryid)))
return None
currentsong = self.songqueue.GetQueue()[0]
if currentsong == None:
logging.warning("There is no song in the queue. So there is also no song with the ID %s! \033[1;30m(RemoveSongFromQueue will be ignored)", entryid)
return None
if currentsong["entryid"] == int(entryid):
logging.debug("Current song cannot be removed from queue because it is currently streamed. Calling PlayNextSong instead.")
self.audiostream.PlayNextSong()
else:
self.songqueue.RemoveSong(int(entryid))
return None
[docs] def RemoveVideoFromQueue(self, entryid):
"""
This method removes a video from the video queue.
The video gets identified by the entry ID of the queue entry.
If the video is currently streamed, playing the next video gets triggered.
Args:
entryid (str) Queue entry ID of the video
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("RemoveVideoFromQueue", {entryid:"82390194629402649"});
"""
if type(entryid) != str:
logging.warning("entryid must be of type string! Actual type was %s. \033[1;30m(RemoveVideoFromQueue will be ignored)", str(type(entryid)))
return None
currentvideo = self.videoqueue.GetQueue()[0]
if currentvideo == None:
logging.warning("There is no video in the queue. So there is also no song with the ID %s! \033[1;30m(RemoveVideoFromQueue will be ignored)", entryid)
return None
if currentvideo["entryid"] == int(entryid):
logging.debug("Current video cannot be removed from queue because it is currently streamed. Calling PlayNextVideo instead.")
self.videostream.PlayNextVideo()
else:
self.videoqueue.RemoveVideo(int(entryid))
return None
[docs] def MoveSongInQueue(self, entryid, afterid):
"""
This is a direct interface to :meth:`musicdb.mdbapi.songqueue.SongQueue.MoveSong`.
It moves a song from one position in the queue to another one.
It is not allowed to move the current playing song (index 0).
When this is tried, nothing happens.
Args:
entryid (str): Position of the song
afterid (str): The position the song shall be moved to
Return:
``None``
Example:
Assuming the following queue:
+----------+---------+
| entry ID | song ID |
+==========+=========+
| 1337 | 1 |
+----------+---------+
| 7357 | 2 |
+----------+---------+
| 2323 | 3 |
+----------+---------+
| 4242 | 4 |
+----------+---------+
Then the two calls will be applied.
The first one is invalid, because entry ID ``"1337"`` addresses the current playing song at the top of the queue.
So that one will be ignored.
The next one moves song 2 to the end of the queue.
.. code-block:: javascript
MusicDB_Call("MoveSongInQueue", {entryid:"1337", afterid:"2323"});
MusicDB_Call("MoveSongInQueue", {entryid:"7357", afterid:"4242"});
So after processing the two calls, the queue looks like this:
+----------+---------+
| entry ID | song ID |
+==========+=========+
| 1337 | 1 |
+----------+---------+
| 2323 | 3 |
+----------+---------+
| 4242 | 4 |
+----------+---------+
| 7357 | 2 |
+----------+---------+
"""
if type(entryid) != str:
logging.warning("entryid must be of type string! Actual type was %s. \033[1;30m(MoveSongInQueue will be ignored)", str(type(entryid)))
return None
if type(afterid) != str:
logging.warning("afterid must be of type string! Actual type was %s. \033[1;30m(MoveSongInQueue will be ignored)", str(type(afterid)))
return None
self.songqueue.MoveSong(int(entryid), int(afterid))
return None
[docs] def MoveVideoInQueue(self, entryid, afterid):
"""
This is a direct interface to :meth:`musicdb.mdbapi.videoqueue.VideoQueue.MoveVideo`.
It moves a video from one position in the queue to another one.
It is not allowed to move the current playing video (index 0).
When this is tried, nothing happens.
Args:
entryid (str): Position of the video
afterid (str): The position the video shall be moved to
Return:
``None``
"""
if type(entryid) != str:
logging.warning("entryid must be of type string! Actual type was %s. \033[1;30m(MoveVideoInQueue will be ignored)", str(type(entryid)))
return None
if type(afterid) != str:
logging.warning("afterid must be of type string! Actual type was %s. \033[1;30m(MoveVideoInQueue will be ignored)", str(type(afterid)))
return None
self.videoqueue.MoveVideo(int(entryid), int(afterid))
return None
[docs] def GetSongRelationship(self, songid):
"""
This method returns the relationship of a song.
It is a list of songs that were played before or after the song with song ID *songid*.
The returned dictionary contains the same song ID given as argument to this method,
as well as the corresponding song, album and artist entry of that song.
Additional the requested list of related songs is given.
Each lists entry is a dictionary with the related ``song``, ``album`` and ``artist`` album from the database.
Furthermore the ``weight`` is given.
The weight indicates how often the two songs were played together.
Hated and disabled songs will not appear in the list.
The list of songs gets sorted by the keys in the following list: (sorted by priority)
* Artist Name
* Album Release
* Album Name
* Song CD
* Song Number
The ``tags`` of that song will also be returned separated into ``genre``, ``subgenre`` and ``mood``.
See :meth:`~GetSongTags` for details how they are returned.
Args:
songid (int): ID so a song
Returns:
A list of related songs
Example:
.. code-block:: javascript
MusicDB_Request("GetSongRelationship", "ShowRelations", {songid:songid});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetSongRelationship" && sig == "ShowRelations")
{
console.log("Songs related to the one with ID " + args.songid);
console.log(" … from the album " + args.album.name);
for(let entry of args.songs)
console.log("Song " + entry.song.name + " with weight " + entry.weight);
console.log(entry.tags.genres)
}
}
"""
# get raw relationship
trackerdb = TrackerDatabase(self.cfg.files.trackerdatabase)
results = trackerdb.GetRelations("song", songid)
parentsid = songid # store for return value
# get all songs
entries = []
#for songid in songids:
for result in results:
entry = {}
songid = result["id"]
weight = result["weight"]
song = self.database.GetSongById(songid)
# Ignore hated and disabled songs
if song["favorite"] == -1 or song["disabled"]:
continue
tags = self.GetSongTags(songid)
entry["song"] = song
entry["tags"] = tags
entry["weight"] = weight
entry["album"] = self.database.GetAlbumById(song["albumid"])
entry["artist"] = self.database.GetArtistById(song["artistid"])
entries.append(entry)
# Sort by Artist-ID and Album-ID
entries.sort(key = lambda k:(
k["artist"]["name"],
k["album"]["release"],
k["album"]["name"],
k["song"]["cd"],
k["song"]["number"]
))
packet = {}
packet["songid"] = parentsid
packet["song"] = self.database.GetSongById( packet["songid"]);
packet["album"] = self.database.GetAlbumById( packet["song"]["albumid"]);
packet["artist"] = self.database.GetArtistById(packet["song"]["artistid"]);
packet["songs"] = entries
return packet
[docs] def GetVideoRelationship(self, videoid):
"""
This method returns the relationship of a video to other videos.
It is a list of videos that were played before or after the video with video ID *videoid*.
The returned dictionary contains the same video ID given as argument to this method, and list of related videos.
Each lists entry is a dictionary with the related ``video`` and ``artist`` from the database.
Furthermore the ``weight`` is given.
The weight indicates how often the two videos were played together.
Hated and disabled videos will not appear in the list.
The list of videos gets sorted by the keys in the following list: (sorted by priority)
* Artist Name
* Video Release
The ``tags`` of that videos will also be returned separated into ``genre``, ``subgenre`` and ``mood``.
See :meth:`~GetVideoTags` for details how they are returned.
Args:
videoid (int): ID so a video
Returns:
A list of related videos
Example:
.. code-block:: javascript
MusicDB_Request("GetVideoRelationship", "ShowRelations", {videoid:7357});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetVideoRelationship" && sig == "ShowRelations")
{
console.log("Videos related to the one with ID " + args.videoid)
for(let entry of args.videos)
console.log("Video " + entry.video.name + " with weight " + entry.weight);
console.log(entry.tags.genres)
}
}
"""
# get raw relationship
trackerdb = TrackerDatabase(self.cfg.files.trackerdatabase)
results = trackerdb.GetRelations("video", videoid)
parentsid = videoid # store for return value
# get all videos
entries = []
for result in results:
entry = {}
videoid= result["id"]
weight = result["weight"]
video = self.database.GetVideoById(videoid)
# Ignore hated and disabled videos
if video["favorite"] == -1 or video["disabled"]:
continue
tags = self.GetVideoTags(videoid)
entry["video"] = video
entry["tags"] = tags
entry["weight"] = weight
entry["artist"] = self.database.GetArtistById(video["artistid"])
entries.append(entry)
# Sort by Artist-ID and Album-ID
entries.sort(key = lambda k:(
k["artist"]["name"],
k["video"]["release"]
))
packet = {}
packet["videoid"] = parentsid
packet["videos"] = entries
return packet
[docs] def GetSongLyrics(self, songid):
"""
This method returns the lyrics of a song.
The returned dictionary contains the same song ID given as argument to this method,
as well as the corresponding song, album and artist entry of that song.
Additional the requested lyrics and the lyricsstate explicitly.
The specification how lyrics are formatted and which states are available can be found in the :mod:`~musicdb.lib.db.musicdb` documentation.
Args:
songid (int): ID so a song
Returns:
The lyrics of the song and additional information
Example:
.. code-block:: javascript
MusicDB_Request("GetSongLyrics", "ShowLyrics", {songid:songid});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetSongLyrics" && sig == "ShowLyrics")
{
console.log("Song lyrics to the one with ID " + args.songid);
console.log(" … from the album " + args.album.name);
console.log(args.lyrics);
console.log("The lyrics state is " + args.lyricsstate);
}
}
"""
result = {}
result["songid"] = songid
result["song"] = self.database.GetSongById(songid);
result["album"] = self.database.GetAlbumById( result["song"]["albumid"]);
result["artist"] = self.database.GetArtistById(result["song"]["artistid"]);
result["lyrics"] = self.database.GetLyrics(songid)
result["lyricsstate"] = result["song"]["lyricsstate"]
return result
[docs] def SetSongLyrics(self, songid, lyrics, state):
"""
This method is a direct interface to :meth:`~musicdb.lib.db.musicdb.MusicDatabase.SetLyrics`.
The specification how lyrics are formatted and which states are available can be found in the :mod:`~musicdb.lib.db.musicdb` documentation.
Args:
songid (int): ID of the song the lyrics belong to
lyrics (str): The lyrics to store
lyricsstate (int): The new state of the lyrics
Returns:
``None``
"""
try:
self.database.SetLyrics(songid, lyrics, state)
except ValueError as e:
logging.warning("Setting Lyrics failed with error: %s", str(e))
except Exception as e:
logging.exception("Setting Lyrics failed with error: %s", str(e))
return None
[docs] def HideAlbum(self, albumid, hide):
"""
Hides or shows an album depending on the *hide* state.
When ``hide == True`` the album gets hidden,
when ``hide == False`` the hidden state gets reset to make the album visible again.
Args:
albumid (int): ID of the album
hide (boolean): Hide or Show the album
Return:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("HideAlbum", {albumid: 1000, hide: false});
"""
if type(hide) != bool:
logging.warning("Hide-state is not a boolean, it is of type %s!", str(type(hide)));
self.database.SetAlbumHiddenState(albumid, hide)
return None
[docs] def SetAlbumOrigin(self, albumid, origin):
"""
This method updates the origin of an album.
The origin must be a string like ``"iTunes"``, ``"Amazon"``, ``"CD"`` or ``"internet"``.
Any string is possible.
The origin describes where the album files come from - where they have be bought.
Usually the string stored in *origin* is the name of the online shop. Avoid URLs.
Args:
albumid (int): ID of the album to update
origin (str): Origin of the album
Returns:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("SetAlbumOrigin", {albumid: 1000, origin: "internet"});
"""
try:
self.database.SetAlbumOrigin(albumid, origin)
except TypeError as e:
logging.warning("SetAlbumOrigin failed with exception: %s", str(e));
return None
[docs] def SetAlbumImportTime(self, albumid, importtime):
"""
This method updates the import time ("added" entry) of an album.
This is the time at which the album has been imported into the MusicDB Database.
It is expected that the ``importtime`` is a unix time stamp in seconds.
Usually this time gets set by the import routines.
Do only change this value if it is obviously wrong.
Args:
albumid (int): ID of the album to update
importtime (str|int): Time at which the album has been imported
Returns:
``None``
Example:
.. code-block:: javascript
let jstimestamp = Date.now(); // milliseconds
let unixtimestamp = Math.floor(jstimestamp / 1000); // seconds
MusicDB_Call("SetAlbumImportTime", {albumid: 1000, importtime: unixtimestamp});
"""
try:
self.database.SetAlbumAddedTime(albumid, int(importtime));
except TypeError as e:
logging.warning("SetAlbumAddedTime called by SetAlbumImportTime failed with exception: %s", str(e));
return None
[docs] def SetAlbumColor(self, albumid, colorname, color):
"""
Sets a color scheme for an album.
Valid color names are the following and must be given as string to the *colorname* parameter.
The color itself must be in HTML-Format: ``#RRGGBB``.
The following color-names exist:
* ``"bgcolor"`` - Background color
* ``"fgcolor"`` - Primary foreground color
* ``"hlcolor"`` - Secondary foreground color
Args:
albumid (int): ID of the album
colorname (str): Name of the color to set (``"fgcolor"``, ``"hlcolor"``, ``"bgcolor"``)
color (str): Color code in HTML notation: #RRGGBB
Return:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("SetAlbumColor", {albumid:1000, colorname:"bgcolor", color:"#42AB23"});
"""
if not colorname in ["bgcolor", "fgcolor", "hlcolor"]:
logging.warning("colorname must be bgcolor, fgcolor or hlcolor");
return False
try:
self.database.SetArtworkColorByAlbumId(albumid, colorname, color)
except ValueError as e:
logging.warning("Update Album Color failed: %s", str(e))
logging.warning(" For AlbumID %s, Colorname %s and Color %s",
str(albumid),
str(colorname),
str(color))
except Exception as e:
logging.exception("Update Album Color failed: %s", str(e))
logging.error(" For AlbumID %s, Colorname %s and Color %s",
str(albumid),
str(colorname),
str(color))
return False
return True
[docs] def SetVideoColor(self, videoid, colorname, color):
"""
Sets a color scheme for a video.
Valid color names are the following and must be given as string to the *colorname* parameter.
The color itself must be in HTML-Format: ``#RRGGBB``.
The following color-names exist:
* ``"bgcolor"`` - Background color
* ``"fgcolor"`` - Primary foreground color
* ``"hlcolor"`` - Secondary foreground color
Args:
videoid (int): ID of the video
colorname (str): Name of the color to set (``"fgcolor"``, ``"hlcolor"``, ``"bgcolor"``)
color (str): Color code in HTML notation: #RRGGBB
Return:
``True`` on success, otherwise false
Example:
.. code-block:: javascript
MusicDB_Call("SetVideoColor", {videoid:1000, colorname:"bgcolor", color:"#42AB23"});
"""
if not colorname in ["bgcolor", "fgcolor", "hlcolor"]:
logging.warning("colorname must be bgcolor, fgcolor or hlcolor");
return False
try:
self.database.SetColorThemeByVideoId(videoid, colorname, color)
except ValueError as e:
logging.warning("Update Video Color failed: %s", str(e))
logging.warning(" For VideoID %s, Colorname %s and Color %s",
str(videoid),
str(colorname),
str(color))
except Exception as e:
logging.exception("Update Video Color failed: %s", str(e))
logging.error(" For VideoID %s, Colorname %s and Color %s",
str(videoid),
str(colorname),
str(color))
return False
return True
[docs] def SetVideoTimeFrame(self, videoid, begin, end):
"""
Set the time frame for a video.
This time frame defines where the player should start playing the video
and where it should end.
Purpose for this is to cut away intros and outros.
The values are floating point numbers an represent the second inside the video file.
In case begin or end is ``None`` (``null`` in JavaScript), they get reset to the default value.
The default for *begin* is ``0.0`` and for *end* is the total play time of the video.
Args:
videoid (int): ID of the video
begin (int): Begin of the main content in seconds.
end (int): End of the main content in seconds.
Return:
``True`` on success, otherwise false
Example:
.. code-block:: javascript
MusicDB_Call("SetVidoTimeFrame", {videoid:1000, begin:23, end:142});
"""
# Validate input
video = self.database.GetVideoById(videoid)
if video == None:
logging.warning("There is no video with ID \"%s\"!", str(videoid))
return False
vbegin = None
if type(begin) == int:
vbegin = begin
elif begin == None:
vbegin = 0.0
else:
try:
vbegin = int(begin)
except ValueError:
logging.warning("Invalid video begin marker \"%s\". An integer was expected.", str(begin))
return False
vend = None
if type(end) == int:
vend = end
elif end == None:
vend = video["playtime"]
else:
try:
vend = int(end)
except ValueError:
logging.warning("Invalid video begin marker \"%s\". An integer was expected.", str(begin))
return False
if vbegin > vend:
logging.warning("Begin of the frame must not be larger than the end! (%s > %s)", str(vbegin), str(vend))
return False
self.database.SetVideoTimeFrame(videoid, vbegin, vend)
return True
[docs] def SetAlbumTag(self, albumid, tagid):
"""
Sets a tag for an album.
This method sets the approval-level to 1 (Set by User) and confidence to 1.0 for this tag.
So, this method can also be used to approve an AI set tag.
If tagging is disabled nothing will be done.
Args:
albumid (int): ID of the album
tagid (int): ID of the tag
Return:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("SetAlbumTag", {albumid:albumid, tagid:tagid});
"""
if self.cfg.debug.disabletagging:
logging.info("Changing tags disabled. \033[1;33m!!")
return None
self.database.SetTargetTag("album", albumid, tagid)
return None
[docs] def RemoveAlbumTag(self, albumid, tagid):
"""
Removes a tag from an album
If tagging is disabled nothing will be done.
Args:
albumid (int): ID of the album
tagid (int): ID of the tag
Return:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("RemoveAlbumTag", {albumid:albumid, tagid:tagid});
"""
if self.cfg.debug.disabletagging:
logging.info("Changing tags disabled. \033[1;33m!!")
return None
self.database.RemoveTargetTag("album", albumid, tagid)
return None
[docs] def SetSongTag(self, songid, tagid, approval=1, confidence=None):
"""
Sets a tag for a song.
This method sets the approval-level to 1 (Set by User) and confidence to 1.0 for this tag by default.
So, this method can also be used to approve an AI set tag.
It is also possible to set a "guesses" tag by setting ``approval`` to ``0`` and ``confidence`` to a value less that ``1.0``.
If tagging is disabled nothing will be done.
After executing this method, the MusicDB server broadcasts the result of :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSong`. (``method = "broadcast", fncname = "GetSong"``)
So each client gets informed about the changes made.
This is important to update the HUD if the song is currently playing.
After setting the new tag for the song,
the album tags get updated using :meth:`musicdb.mdbapi.tags.MusicDBTags.DeriveAlbumTags`.
So when changing a genre or sub genre, then they get suggested as album genre or sub genre
Args:
songid (int): ID of the song
tagid (int): ID of the tag
apprival (int): (optional) ``0``: for "just a guess", not approved. ``1``: Approved by the user.
confidence (float): (optional) A value between ``0.0`` and ``1.0`` representing the confidence that an none-approved tag is valid.
Return:
``None``
Examples:
.. code-block:: javascript
MusicDB_Call("SetSongTag", {songid:songid, tagid:tagid});
.. code-block:: javascript
MusicDB_Call("SetSongTag", {songid:songid, tagid:tagid, approval:0, confidence:0.5});
.. code-block:: javascript
MusicDB_Request("SetSongTag", "UpdateTagInput", {songid:songid, tagid:tagid}, {taginputid:taginputid});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetSong" && sig == "UpdateTagInput")
{
console.log("Updating " + pass.taginputid + " for song " + args.song.name);
Taginput_Update(pass.taginputid, args.tags);
}
}
"""
if self.cfg.debug.disabletagging:
logging.info("Changing tags disabled. \033[1;33m!!")
return None
self.database.SetTargetTag("song", songid, tagid, approval, confidence)
albumid = self.database.GetSongById(songid)["albumid"]
self.tags.DeriveAlbumTags(albumid)
return None
[docs] def RemoveSongTag(self, songid, tagid):
"""
Removes a tag from a song
If tagging is disabled nothing will be done.
After executing this method, the MusicDB server broadcasts the result of :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSong`. (``method = "broadcast", fncname = "GetSong"``)
So each client gets informed about the changes made.
This is important to update the HUD if the song is currently playing.
After setting the new tag for the song,
the album tags get updated using :meth:`musicdb.mdbapi.tags.MusicDBTags.DeriveAlbumTags`.
So when changing a genre or sub genre, then they get suggested as album genre or sub genre
Args:
songid (int): ID of the song
tagid (int): ID of the tag
Return:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("RemoveSongTag", {songid:songid, tagid:tagid});
"""
if self.cfg.debug.disabletagging:
logging.info("Changing tags disabled. \033[1;33m!!")
return None
self.database.RemoveTargetTag("song", songid, tagid)
albumid = self.database.GetSongById(songid)["albumid"]
self.tags.DeriveAlbumTags(albumid)
return None
[docs] def SetVideoTag(self, videoid, tagid):
"""
Sets a tag for a video.
This method sets the approval-level to 1 (Set by User) and confidence to 1.0 for this tag.
So, this method can also be used to approve an AI set tag.
If tagging is disabled nothing will be done.
After executing this method, the MusicDB server broadcasts the result of :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideo`. (``method = "broadcast", fncname = "GetVideo"``)
So each client gets informed about the changes made.
This is important to update the HUD if the video is currently playing.
Args:
videoid (int): ID of the video
tagid (int): ID of the tag
Return:
``None``
Examples:
.. code-block:: javascript
MusicDB_Call("SetVideoTag", {videoid:videoid, tagid:tagid});
.. code-block:: javascript
MusicDB_Request("SetVideoTag", "UpdateTagInput", {videoid:videoid, tagid:tagid}, {taginputid:taginputid});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "GetVideo" && sig == "UpdateTagInput")
{
console.log("Updating " + pass.taginputid + " for video " + args.video.name);
Taginput_Update(pass.taginputid, args.tags);
}
}
"""
if self.cfg.debug.disabletagging:
logging.info("Changing tags disabled. \033[1;33m!!")
return None
self.database.SetTargetTag("video", videoid, tagid)
return None
[docs] def RemoveVideoTag(self, videoid, tagid):
"""
Removes a tag from a video.
If tagging is disabled nothing will be done.
After executing this method, the MusicDB server broadcasts the result of :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideo`. (``method = "broadcast", fncname = "GetVideo"``)
So each client gets informed about the changes made.
This is important to update the HUD if the video is currently playing.
Args:
videoid (int): ID of the video
tagid (int): ID of the tag
Return:
``None``
Example:
.. code-block:: javascript
MusicDB_Call("RemoveVideoTag", {videoid:videoid, tagid:tagid});
"""
if self.cfg.debug.disabletagging:
logging.info("Changing tags disabled. \033[1;33m!!")
return None
self.database.RemoveTargetTag("video", videoid, tagid)
return None
[docs] def AddGenre(self, name):
"""
This method creates a new genre.
If the tag already exists, nothing happens.
After executing this command, :meth:`~GetTags` gets executed.
Its return value gets send via broadcast.
If tagging is disabled nothing will be changed.
The broadcast gets send anyway.
Args:
name (str): Name of the new genre
"""
if self.cfg.debug.disabletagging:
logging.info("Changing tags disabled. \033[1;33m!!")
return None
self.database.CreateTag(name, MusicDatabase.TAG_CLASS_GENRE)
return None
[docs] def AddSubgenre(self, name, parentname):
"""
This method creates a new subgenre.
If the tag already exists, nothing happens.
After executing this command, :meth:`~GetTags` gets executed.
Its return value gets send via broadcast.
If tagging is disabled nothing will be changed.
The broadcast gets send anyway.
Args:
name (str): Name of the new subgenre
parentname (str): Name of the main genre
"""
if self.cfg.debug.disabletagging:
logging.info("Changing tags disabled. \033[1;33m!!")
return None
parenttag = self.database.GetTagByName(parentname, MusicDatabase.TAG_CLASS_GENRE)
if not parenttag:
logging.warning("There is no maingenre called \"%s\"! \033[1;30m(Adding subgenre \"%s\" canceled)", parentname, name)
return None
self.database.CreateTag(name, MusicDatabase.TAG_CLASS_SUBGENRE, parenttag["id"])
return None
[docs] def AddMoodFlag(self, name, icon, color, posx, posy):
"""
This method creates a new Mood Flag.
If the tag already exists, nothing happens.
See :mod:`~musicdb.lib.db.musicdb` for details about the attributes!
If *color* is ``null``, no color is used for the flag.
This should be the default case, because the mood flags should adopt to the WebUI color scheme.
Otherwise the color must be given in 7-character HTML notation: ``#RRGGBB``.
The icon type gets derived from the content of the *icon* parameter via :meth:`musicdb.mdbapi.tags.MusicDBTags.AnalyseIcon`
After executing this command, :meth:`~GetTags` gets executed.
Its return value gets send via broadcast.
If tagging is disabled nothing will be changed.
The broadcast gets send anyway.
Args:
name (str): Name of the new subgenre
icon (str): Icon for that flag
color (str): (optional) HTML Color
posx (int): X-Position in the moods-grid
posy (int): X-Position in the moods-grid
"""
if self.cfg.debug.disabletagging:
logging.info("Changing tags disabled. \033[1;33m!!")
return None
tag = self.database.GetTagByName(name, MusicDatabase.TAG_CLASS_MOOD)
if tag:
logging.warning("There is already a Mood-Flag called \"%s\"! \033[1;30m(Adding mood \"%s\" canceled)", name, name)
return None
self.tags.CreateMood(name)
self.tags.ModifyMood(name, None, icon, color, posx, posy)
return None
[docs] def DeleteTag(self, tagid):
"""
This method deletes a tag addressed by its tag ID.
.. warning::
If the tag is a Genre, its sub-genres will be deleted as well!
Before deleting the tag, this tag as well as its child-tags (sub-genre tag) will be removed from all
songs, albums and videos.
After executing this command, :meth:`~GetTags` gets executed.
Its return value gets send via broadcast.
If tagging is disabled nothing will be changed.
The broadcast gets send anyway.
Args:
name (str): Name of the new subgenre
parentname (str): Name of the main genre
"""
if self.cfg.debug.disabletagging:
logging.info("Changing tags disabled. \033[1;33m!!")
return None
self.database.DeleteTagById(tagid)
return None
[docs] def ModifyTag(self, tagid, attribute, value):
"""
This method allows to modify most of the attributes of a tag.
The *tagid* addresses the tag, *attribute* the attribute.
*value* is the new attribute set for the tag.
The following attributes are allowed: ``"name"`` ``"icon"`` ``"icontype"`` ``"color"`` ``"posx"`` ``"posy"``.
See :mod:`~musicdb.lib.db.musicdb` for details about the attributes!
In case the icon gets modified, the icon-type will be updated automatically via :meth:`musicdb.mdbapi.tags.MusicDBTags.AnalyseIcon`
After executing this command, :meth:`~GetTags` gets executed.
Its return value gets send via broadcast.
If tagging is disabled nothing will be changed.
The broadcast gets send anyway.
Args:
tagid (int): ID of the tag to modify
attribute (str): The name of the attribute that shall be modified
newvalue: The new value. Read the introduction at the top of the document to see what values are possible for a specific attribute
"""
if self.cfg.debug.disabletagging:
logging.info("Changing tags disabled. \033[1;33m!!")
return None
self.database.ModifyTagById(tagid, attribute, value)
if attribute == "icon":
icontype, _ = self.tags.AnalyseIcon(value)
self.database.ModifyTagById(tagid, "icontype", icontype)
return None
[docs] def UpdateSongStatistic(self, songid, statistic, modifier):
"""
When this method got called direct via the JavaScript API, the MusicDB server broadcasts the result of :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSong`. (``method = "broadcast", fncname = "GetSong"``)
So each client gets informed about the changes made.
This is important to update the HUD if the song is currently playing.
This method is a direct interface to :meth:`musicdb.lib.db.musicdb.MusicDatabase.UpdateSongStatistic`. See the related documentation for more details.
"""
if self.cfg.debug.disablestats:
logging.info("Updating song statistics disabled. \033[1;33m!!")
return None
try:
self.database.UpdateSongStatistic(songid, statistic, modifier)
except ValueError as e:
logging.warning("Updating song statistics failed with error: %s!", str(e))
except Exception as e:
logging.exception("Updating song statistics failed with error: %s!", str(e))
return None
[docs] def CutSongRelationship(self, songid, relatedsongid):
"""
This method removes the relation between two songs.
After executing this method, the MusicDB server broadcasts the result of :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSongRelationship`. (``method = "broadcast", fncname = "GetSongRelationship"``)
So each client gets informed about the changes made.
Args:
songid (int): ID of one song
relatedsongid (int): ID of the related song
"""
if self.cfg.debug.disabletracker:
logging.info("Updating tracker disabled. \033[1;33m!!")
return None
try:
trackerdb = TrackerDatabase(self.cfg.files.trackerdatabase)
trackerdb.RemoveRelation("song", songid, relatedsongid)
except Exception as e:
logging.warning("Removing song relations failed with error: %s", str(e))
return None
[docs] def CutVideoRelationship(self, videoid, relatedvideoid):
"""
This method removes the relation between two videos.
After executing this method, the MusicDB server broadcasts the result of :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetVideoRelationship`. (``method = "broadcast", fncname = "GetVideoRelationship"``)
So each client gets informed about the changes made.
Args:
videoid (int): ID of one video
relatedvideoid (int): ID of the related video
"""
if self.cfg.debug.disabletracker:
logging.info("Updating tracker disabled. \033[1;33m!!")
return None
try:
trackerdb = TrackerDatabase(self.cfg.files.trackerdatabase)
trackerdb.RemoveRelation("video", videoid, relatedvideoid)
except Exception as e:
logging.warning("Removing video relations failed with error: %s", str(e))
return None
[docs] def RemoveSongEntry(self, songid):
"""
This method removes a song from the database.
The related file in the Music Directory remains untouched.
All other information related to the song entry will be removed from the database as well.
Args:
songid (int): ID of the song to remove
Example:
.. code-block:: javascript
MusicDB_Call("RemoveSongEntry", {songid: 23});
"""
try:
self.music.RemoveSong(songid)
except Exception as e:
logging.warning("Removing song entry (%s) from database failed with error: %s", str(songid), str(e))
return None
[docs] def CreateSongEntry(self, newpath):
"""
This method creates a new song entry in the database.
The song file is addressed by *newpath*.
This file must fulfill the MusicDB name scheme for song files.
The import is done via :meth:`musicdb.mdbapi.music.MusicDBMusic.AddSong`.
It is important that the album already exists in the database.
This method does not replace the :meth:`~InitiateMusicImport` which imports new albums or videos.
This method is supposed to be used to add a new song to an existing album.
The album entry gets updated as well (number of songs).
Args:
newpath (str): Path to the new song file relative to the music directory
Example:
.. code-block:: javascript
MusicDB_Call("CreateSongEntry", {newpath: "Artist/2020 - Album/01 Updated Song.flac"});
"""
try:
self.music.AddSong(newpath)
except Exception as e:
logging.warning("Creating new song entry for \"%s\" failed with error: %s", str(newpath), str(e))
return None
[docs] def UpdateSongEntry(self, songid, newpath):
"""
This method updates the database entry of the song with the ID *songid*.
The information for the update come from the song file addressed by *newpath*.
The update is done via :meth:`musicdb.mdbapi.music.MusicDBMusic.UpdateSong`.
Args:
songid (int): ID of the song to update
newpath (str): Path to the new song file relative to the music directory
Example:
.. code-block:: javascript
MusicDB_Call("UpdateSongEntry", {songid: 23, newpath: "Artist/2020 - Album/01 Updated Song.flac"});
"""
try:
self.music.UpdateSong(songid, newpath)
except Exception as e:
logging.warning("Updating song entry (%s) to \"%s\" failed with error: %s", str(songid), str(newpath), str(e))
return None
[docs] def UpdateAlbumEntry(self, albumid, newpath):
"""
This method updates the database entry of the album with the ID *albumid*.
The information for the update come from the directory addressed by *newpath*.
The update is done via :meth:`musicdb.mdbapi.music.MusicDBMusic.UpdateAlbum`.
All songs inside the directory get updated via :meth:`musicdb.mdbapi.music.MusicDBMusic.UpdateSong` as well.
Args:
songid (int): ID of the album to update
newpath (str): Path to the new album directory relative to the music directory
Example:
.. code-block:: javascript
MusicDB_Call("UpdateAlbumEntry", {albumid: 1337, newpath: "Artist/2020 - Updated Album"});
"""
try:
self.music.UpdateAlbum(albumid, newpath)
except Exception as e:
logging.warning("Updating album entry (%s) to \"%s\" failed with error: %s", str(albumid), str(newpath), str(e))
return None
[docs] def RemoveAlbumEntry(self, albumid):
"""
This method removes an album and all its songs from the database.
The related directory and files in the Music Directory remains untouched.
All other information related to the songs will be removed from the database as well.
Args:
albumid (int): ID of the album to remove
Example:
.. code-block:: javascript
MusicDB_Call("RemoveAlbumEntry", {albumid: 42});
"""
try:
self.music.RemoveAlbum(albumid)
except Exception as e:
logging.warning("Removing album entry (%s) from database failed with error: %s", str(albumid), str(e))
return None
[docs] def UpdateArtistEntry(self, artistid, newpath):
"""
This method updates the database entry of the artist with the ID *artistid*.
The information for the update come from the directory addressed by *newpath*.
The update is done via :meth:`musicdb.mdbapi.music.MusicDBMusic.UpdateArtist`.
All albums inside the directory get updated via :meth:`musicdb.mdbapi.music.MusicDBMusic.UpdateAlbum` as well as
all their songs via :meth:`musicdb.mdbapi.music.MusicDBMusic.UpdateSong`.
Args:
artistid (int): ID of the artist to update
newpath (str): Path to the new artist directory relative to the music directory
Example:
.. code-block:: javascript
MusicDB_Call("UpdateArtisEntry", {artistid: 1337, newpath: "Moved Artist"});
"""
try:
self.music.UpdateArtist(artistid, newpath)
except Exception as e:
logging.warning("Updating artist entry (%s) to \"%s\" failed with error: %s", str(artistid), str(newpath), str(e))
return None
[docs] def RemoveArtistEntry(self, artistid):
"""
This method removes an artist and all its albums and songs from the database.
The related directory and files in the Music Directory remains untouched.
All other information related to the songs will be removed from the database as well.
Args:
artistid (int): ID of the album to remove
Example:
.. code-block:: javascript
MusicDB_Call("RemoveArtistEntry", {artistid: 42});
"""
try:
self.music.RemoveArtist(artistid)
except Exception as e:
logging.warning("Removing artist entry (%s) from database failed with error: %s", str(artistid), str(e))
return None
[docs] def FindNewContent(self):
"""
This method uses :meth:`musicdb.mdbapi.music.MusicDBMusic.FindNewPaths` to get all new albums and videos.
The lists of albums and videos contain objects with the following keys:
* For Videos:
* ``"path"``: Path to the new video
* ``"artistname"``:
* ``"videoname"``:
* ``"release"``:
* ``"extension"``:
* For Albums:
* ``"path"``: Path to the new album
* ``"artistname"``:
* ``"albumname"``:
* ``"release"``:
Returns:
A dict with two listst: ``"albums"`` and ``"videos"``. Each list entry is another object with the key listed in the description.
Example:
.. code-block:: javascript
MusicDB_Request("FindNewContent", "ListContent");
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "FindNewContent" && sig == "ListContent")
{
console.log(args.albums);
console.log(args.videos);
}
}
"""
paths = self.music.FindNewPaths()
albumpaths = paths["albums"]
videopaths = paths["videos"]
# Transcode Path objects to strings
albumpaths = [str(path) for path in albumpaths]
videopaths = [str(path) for path in videopaths]
newcontent = {}
newcontent["albums"] = []
newcontent["videos"] = []
musicdirectory = MusicDirectory(self.cfg)
for path in videopaths:
entry = {}
# Try analyse path. If it fails, assume infos
infos = musicdirectory.AnalysePath(path)
if infos == None:
infos = {}
infos["artist"] = "".join(path.split("/")[0])
infos["video"] = "".join(path.split("/")[1:])
infos["video"] = "".join(infos["video"].split(".")[:-1]) # remove file extension
infos["release"] = None
infos["extension"] = "".join(path.split(".")[-1])
entry["path"] = path
entry["artistname"] = infos["artist"]
entry["videoname"] = infos["video"]
entry["release"] = infos["release"]
entry["extension"] = infos["extension"]
newcontent["videos"].append(entry)
for path in albumpaths:
entry = {}
infos = musicdirectory.AnalysePath(path)
if infos == None:
infos = {}
infos["artist"] = "".join(path.split("/")[0])
albumdirectory = "".join(path.split("/")[1:])
infos["album"] = "".join(albumdirectory.split("-")[0].strip())
infos["release"] = "-".join(albumdirectory.split("-")[1:]).strip()
infos["release"] = None
entry["path"] = path
entry["artistname"] = infos["artist"]
entry["albumname"] = infos["album"]
entry["release"] = infos["release"]
newcontent["albums"].append(entry)
return newcontent
[docs] def FindAlbumSongFiles(self, albumpath):
"""
This method returns a list of song files and information from the given album path.
.. warning::
Because this method is supposed to be used before the album has been imported into MusicDB,
it is not checked and guaranteed that the returned paths are fulfilling the MusicDB naming scheme
(See :doc:`/usage/music`).
The album path can address an album that has not been imported into the MusicDB yet.
The whole method only works on file system level and does not interact with the database.
Beside the paths to the song files (relative to the music directory),
some additional meta data is provided.
These data can be used to validate the naming of the paths because they are not necessarily fulfilling the
MusicDB music naming scheme.
These additional data may also be wrong.
.. note::
Before importing a song, a user has to validate the data returned by this method.
If the album path is not a valid directory inside the music directory, an empty list gets returned.
Each list entry is a dictionary with the following information:
* ``path`` (string): relative path to the music file
* ``frommeta`` (dict): Song information collected from the files meta data
* ``songname`` (string)
* ``albumname`` (string)
* ``artistname`` (string)
* ``releaseyear`` (integer)
* ``origin`` (string): Where the files comes from. Where it has been bought.
* ``cdnumber`` (integer)
* ``songnumber`` (integer)
* ``haslyrics`` (boolean): ``True`` when there are lyrics attached to the song file
* ``hasartwork`` (boolean): ``True`` when there is an album cover attached to the song file
* ``frompath`` (dict): Song information collected from the file path (keep in mind that this path is not yet verified by the user to be correct).
* ``songname`` (string)
* ``albumname`` (string)
* ``artistname`` (string)
* ``releaseyear`` (integer)
* ``cdnumber`` (integer)
* ``songnumber`` (integer)
The information inside the ``"frompath"`` dictionary is collected via :meth:`musicdb.mdbapi.musicdirectory.MusicDirectory.AnalysePath`.
When this method fails, ``None`` is stored under that key.
Args:
albumpath (str): path to an album that may have new/unknown songs. The path must be relative to the music directory.
Returns:
A list of song file information of an album directory
"""
if not self.fs.IsDirectory(albumpath):
return []
files = self.music.FindNewSongs(albumpath)
metadata = MetaTags(self.cfg.directories.music)
songfiles = []
for path in files:
# Read meta data
metadata.Load(path)
frommeta = {}
frommeta["songname"] = metadata.GetSongname()
frommeta["albumname"] = metadata.GetAlbumname()
frommeta["artistname"] = metadata.GetArtistname()
frommeta["releaseyear"] = metadata.GetReleaseyear()
frommeta["origin"] = metadata.GetOrigin()
frommeta["cdnumber"] = metadata.GetCDNumber()
frommeta["songnumber"] = metadata.GetTracknumber()
frommeta["haslyrics"] = type(metadata.GetLyrics()) == str
frommeta["hasartwork"] = metadata.CheckArtwork()
filedata = self.musicdirectory.AnalysePath(path)
if filedata:
fromfile = {}
fromfile["songname"] = filedata["song"]
fromfile["albumname"] = filedata["album"]
fromfile["artistname"] = filedata["artist"]
fromfile["releaseyear"] = filedata["release"]
fromfile["cdnumber"] = filedata["cdnumber"]
fromfile["songnumber"] = filedata["songnumber"]
else:
fromfile = None
entry = {}
entry["path"] = str(path)
entry["frommeta"] = frommeta
entry["fromfile"] = fromfile
songfiles.append(entry)
return songfiles
[docs] def RenameMusicFile(self, oldpath, newpath):
"""
Renames a song or video file.
In general it is not checked if the new path fulfills the Music Naming Scheme (See :doc:`/usage/music`).
If the song has an entry in the Music Database, its entry is updated as well.
In this case the new path must fulfill the Music Naming Scheme.
The position of the file should be plausible anyway.
So a song file should be inside a artist/album/-directory, a video file inside an artist directory.
The complete path, relative to the Music Directory must be given.
Anyway, the artist and album directories must not be different.
Only the file name can be different.
If the old path does not address a file, the Method returns ``False``.
If the new path does address an already existing file, the method returns ``False`` as well.
No files will be overwritten.
Args:
oldpath (str): Path to the file that shall be renamed
newpath (str): New name of the file
Returns:
``True`` on success, otherwise ``False``
Example:
.. code-block:: javascript
// Will succeed
MusicDB_Call("RenameMusicFile", {
oldpath:"Artist/2021 - Album Name/01 old file name.mp3",
newpath:"Artist/2021 - Album Name/01 new file name.mp3"
});
// Will succeed, if the song has no entry in the database.
// Otherwise it fails because the file names violate the naming scheme.
MusicDB_Call("RenameMusicFile", {
oldpath:"Artist/2021 - Album Name/old file name.mp3",
newpath:"Artist/2021 - Album Name/new file name.mp3"
});
// Will fail if because album name changed as well
MusicDB_Request("RenameMusicFile", "ConfirmRename", {
oldpath:"Artist/Old Album Name, Old Song Name.flac",
newpath:"Artist/2021 - New Album Name/01 New Song Name.flac"
},
{
oldfile:"Old Album Name, Old Song Name.flac"
});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "RenameMusicFile" && sig == "ConfirmRename")
{
if(args === true)
console.log(`Renaming ${pass.oldfile} succeeded.`);
}
}
"""
# Determine if the music file could be a song or a video
oldcontenttype = self.musicdirectory.EstimateContentTypeByPath(oldpath)
# Check if exists in database and rename
databaseentry = None
success = False
if oldcontenttype == "song":
databaseentry = self.database.GetSongByPath(oldpath)
success = self.musicdirectory.RenameSongFile(oldpath, newpath)
elif oldcontenttype == "video":
databaseentry = self.database.GetVideoByPath(oldpath)
success = self.musicdirectory.RenameSongFile(oldpath, newpath)
else:
logging.warning("Old path (\"%s\") was estimated as \"%s\". \"song\" or \"video\" was expected. \033[1;30m(Old path will not be renamed)", oldpath, oldcontenttype)
return False
# Check if renaming succeeded. If not an error has already been logged
if success == False:
return False
# Update database if needed
if databaseentry != None:
targetid = databaseentry["id"]
logging.info("Updating database entry for \033[0;36m%s\033[1;34m with ID \033[0;36m%s", oldcontenttype, str(targetid))
if oldcontenttype == "song":
self.music.UpdateSong(targetid, newpath)
elif oldcontenttype == "video":
self.music.UpdateVideo(targetid, newpath)
return True
[docs] def RenameAlbumDirectory(self, oldpath, newpath):
"""
Renames an album directory.
In general it is not checked if the new path fulfills the Music Naming Scheme (See :doc:`/usage/music`).
If the album has an entry in the Music Database, its entry is updated as well.
In this case the new path must fulfill the Music Naming Scheme.
The update triggers :meth:`musicdb.mdbapi.music.MusicDBMusic.UpdateAlbum`.
This leads to updating the path, name, release and origin of an album.
The position of the album should be plausible anyway.
So it must be placed inside an artist directory.
The complete path, relative to the Music Directory must be given.
Anyway, the artist directories must not be different.
Only the album directory name can be different.
If the old path does not address a directory, the Method returns ``False``.
If the new path does address an already existing file or directory, the method returns ``False`` as well.
No files will be overwritten.
Args:
oldpath (str): Path to the directory that shall be renamed
newpath (str): New name of the file
Returns:
``True`` on success, otherwise ``False``
Example:
.. code-block:: javascript
// Will succeed
MusicDB_Call("RenameAlbumDirectory", {
oldpath:"Artist/2021 - Old Album Name",
newpath:"Artist/2021 - New Album Name"
});
// Will succeed, if the album has no entry in the database.
// Otherwise it fails because the file names violate the naming scheme.
MusicDB_Call("RenameAlbumDirectory", {
oldpath:"Artist/Old Album Name",
newpath:"Artist/New Album Name"
});
// Will fail because artist name changed as well.
MusicDB_Request("RenameAlbumDirectory", "ConfirmRename", {
oldpath:"Artist/Old Album Name",
newpath:"New Artist/2021 - New Album Name"
},
{
oldfile:"Old Album Name, Old Song Name.flac"
});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "RenameAlbumDirectory" && sig == "ConfirmRename")
{
if(args === true)
console.log(`Renaming ${pass.oldfile} succeeded.`);
}
}
"""
# Check if exists in database and rename
databaseentry = self.database.GetAlbumByPath(oldpath)
success = self.musicdirectory.RenameAlbumDirectory(oldpath, newpath)
# Check if renaming succeeded. If not an error has already been logged
if not success:
return False
# Update database if needed
if databaseentry != None:
targetid = databaseentry["id"]
logging.info("Updating database entry for \033[0;36mAlbum\033[1;34m with ID \033[0;36m%s", str(targetid))
self.music.UpdateAlbum(targetid, newpath)
return True
[docs] def RenameArtistDirectory(self, oldpath, newpath):
"""
Renames an artist directory.
In general it is not checked if the new path fulfills the Music Naming Scheme (See :doc:`/usage/music`).
If the artist has an entry in the Music Database, its entry is updated as well.
In this case the new path must fulfill the Music Naming Scheme.
The update triggers :meth:`musicdb.mdbapi.music.MusicDBMusic.UpdateArtist`.
This leads to updating the path, name, release and origin of all albums inside the artist directory.
The position of the artist should be plausible anyway.
So it must be placed inside the music directory and not being a sub directory.
If the old path does not address a directory, the Method returns ``False``.
If the new path does address an already existing file or directory, ``False`` gets returned.
No files will be overwritten.
To change the artist directory of an album see :meth:`~RenameArtistDirectory`.
Args:
oldpath (str): Path to the directory that shall be renamed
newpath (str): New name of the artist directory
Returns:
``True`` on success, otherwise ``False``
Example:
.. code-block:: javascript
// Will succeed
MusicDB_Request("RenameArtistDirectory", "ConfirmRename", {
oldpath:"Old Artist",
newpath:"New Artist"
});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "RenameArtistDirectory" && sig == "ConfirmRename")
{
if(args === true)
console.log(`Renaming ${pass.oldfile} succeeded.`);
}
}
"""
# Check if exists in database and rename
databaseentry = self.database.GetArtistByPath(oldpath)
success = self.musicdirectory.RenameArtistDirectory(oldname, newname)
if not success:
return False
# Update database if needed
if databaseentry != None:
targetid = databaseentry["id"]
logging.info("Updating database entry for \033[0;36mArtist\033[1;34m with ID \033[0;36m%s", str(targetid))
self.music.UpdateArtist(targetid, newpath)
return True
[docs] def CreateArtistEntry(self, name):
"""
This method creates a new Artist with the name ``name``.
If there is no directory with the same name in the music root directory,
a new directory will be created.
When creating the directory fails, so that there exists no directory with the artist name,
``None`` will be returned.
If there is already an artist with the same name existing in the database,
its database entry will be returned.
If there the artist is unknown, a new entry will be created.
The new entry will be returned.
For details see :meth:`musicdb.mdbapi.music.MusicDBMusic.CreateNewArtist`.
Args:
name (str): Name of the new artist.
Returns:
The artist entry, or ``None`` on error.
"""
artist = self.music.CreateNewArtist(name)
return artist
[docs] def ChangeArtistDirectory(self, oldalbumpath, newartistdirectory):
"""
This method changes the artist directory of an album.
If the new artist directory is not existing it will be created.
If it exists, the album directory will just be moved into the existing artist directory.
The old artist directory will not be touched, even if it becomes an empty directory.
To rename an artist directory see :meth:`~RenameArtistDirectory`.
If the album exists in the Music Database, its entry will be updated (including its songs).
In this case, a possibly new artist will be imported into the database as well.
If the album does not exists in the database, only the Music Directory will be updated.
The complete album path, relative to the Music Directory must be given.
If the old path does not address a directory, ``False`` gets returned.
No files will be overwritten.
In case the album addressed by *oldalbumpath* exists in the database,
the function :meth:`musicdb.mdbapi.music.MusicDBMusic.ChangeAlbumArtist` is used to keep the database updated.
Args:
oldalbumpath (str): Path to the album directory inside the old artists directory
newartistdirectory (str): Name of the new artist directory in that the addressed album will be moved in.
Returns:
``True`` on success, otherwise ``False``
Example:
.. code-block:: javascript
// Will succeed
MusicDB_Call("ChangeArtistDirectory", {
oldalbumpath: "Old Artist/2021 - Album Name",
newartistdirectory: "Already Existing Artist"
});
// Will succeed
MusicDB_Call("ChangeArtistDirectory", {
oldalbumpath: "Old Artist/2021 - Album Name",
newartistdirectory: "New Artist"
});
// Will succeed if album is known by the database
MusicDB_Call("ChangeArtistDirectory", {
oldalbumpath: "Old Artist/2021 - Album Name", // That album has already been moved by the user
newartistdirectory: "New Artist"
});
// Will fail because new artist directory contains a sub directory
MusicDB_Request("ChangeArtistDirectory", "ConfirmRename", {
oldalbumpath: "Old Artist/2021 - Album Name",
newartistdirectory: "New Artist/subdirectory"
});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(fnc == "ChangeArtistDirectory" && sig == "ConfirmRename")
{
if(args === true)
console.log(`Renaming ${pass.oldfile} succeeded.`);
}
}
"""
# Check if new path addresses an already existing file
if self.musicdirectory.IsFile(newartistdirectory):
logging.warning("Changing Artist Directory failed because new path \"%s\" is an exist file inside the Music Directory", str(newartistdirectory))
return False
# Check if path if path is plausible
oldcontenttype = self.musicdirectory.EstimateContentTypeByPath(oldalbumpath)
newcontenttype = self.musicdirectory.EstimateContentTypeByPath(newartistdirectory)
if oldcontenttype != "album":
logging.warning("Old album path (\"%s\") was estimated as \"%s\". An album was expected. \033[1;30m(Old path will not be renamed)", oldalbumpath, oldcontenttype)
return False
if newcontenttype != "artist":
logging.warning("New artist directory (\"%s\") was estimated as \"%s\". An artist directory was expected. \033[1;30m(Old path will not be renamed)", newartistdirectory, newcontenttype)
return False
# Check if album exists in database or if this is only a "file system job"
logging.info("Moving \033[0;36m%s\033[1;34m ➜ \033[0;36m%s", oldalbumpath, newartistdirectory)
albumentry = self.database.GetAlbumByPath(oldalbumpath)
if albumentry:
# Use proper API
albumid = albumentry["id"]
success = self.music.ChangeAlbumArtist(albumid, newartistdirectory)
else:
# Album does not exist, so only work on file system level
# Check if old path is a valid path to a file in the Music Directory
if not self.musicdirectory.IsDirectory(oldalbumpath):
logging.warning("Changing Artist Directory failed because album path \"%s\" does not exists inside the Music Directory", str(oldalbumpath))
return False
# Check if artist exists. Create if not.
if not self.musicdirectory.IsDirectory(newartistdirectory):
self.musicdirectory.CreateSubdirectory(newartistdirectory)
# Move album into new artist directory
try:
success = self.musicdirectory.MoveDirectory(oldalbumpath, newartistdirectory)
except Exception as e:
logging.error("Moving Directory \"%s\" into \"%s\" failed with exception: %s \033[1;30m(Nothing changed, old path is still valid)", oldalbumpath, newartistdirectory, str(e))
success = False
if not success:
logging.warning("Moving Directory \"%s\" into \"%s\" failed. \033[1;30m(Nothing changed, old path is still valid)", oldalbumpath, newartistdirectory)
return False
return True
[docs] def InitiateUpload(self, taskid, mimetype, contenttype, filesize, checksum, filename):
"""
This method uses :meth:`musicdb.mdbapi.uploadmanager.UploadManager.InitiateUpload`.
Args:
taskid (str): Unique ID to identify the upload task
mimetype (str): MIME-Type of the file (example: ``"image/png"``)
contenttype (str): Type of the content: (``"video"``, ``"album"``, ``"artwork"``)
filesize (int): Size of the complete file in bytes
checksum (str): SHA-1 check sum of the source file
sourcefilename (str): File name (example: ``"test.png"``)
Returns:
The task ID ``taskid``
Example:
.. code-block:: javascript
// TODO
"""
self.uploadmanager.InitiateUpload(taskid, mimetype, contenttype, filesize, checksum, filename)
return taskid
[docs] def UploadChunk(self, taskid, chunkdata):
"""
Args:
taskid (str): Unique ID to identify the upload task
chunkdata (str): Hex-string of the chunk
"""
#import base64
#rawdata = bytes(base64.b64decode(chunkdata))
rawdata = bytes.fromhex(chunkdata) # TODO: JavaScript does not provide a better way
self.uploadmanager.NewChunk(taskid, rawdata);
return
[docs] def GetCurrentTasks(self):
"""
This method gets all tasks from the :mod:`~musicdb.taskmanagement.taskmanager`.
This list then gets split into three lists:
* *videos*: A list of all video related tasks
* *albumfiles*: A song or any other album content processed as part of an album upload
* *artworks*: Artwork uploads and import
* *any*: Can be anything. Depends on the task.
Returns:
a list with information about all yet unprocessed uploads
Example:
.. code-block:: javascript
MusicDB_Request("GetCurrentTasks", "ShowTasks");
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
console.log(args.albumfiles);
console.log(args.albums);
console.log(args.videos);
console.log(args.artworks);
console.log(args["any"]);
}
"""
tasksdict = self.taskmanager.GetTasks()
retval = {}
retval["videos"] = []
retval["albumfiles"] = []
retval["albums"] = []
retval["artworks"] = []
retval["any"] = []
for key, task in tasksdict.items():
contenttype = task["contenttype"]
if contenttype == "video":
contentlist = "videos"
elif contenttype == "albumfile":
contentlist = "albumfiles"
elif contenttype == "album":
contentlist = "albums"
elif contenttype == "artwork":
contentlist = "artworks"
elif contenttype == "any":
contentlist = "any"
else:
logging.debug("Unexpected content type \"%s\" will be ignored", contenttype);
continue
retval[contentlist].append(task)
return retval
[docs] def AnnotateUpload(self, taskid, annotations):
"""
Adds some information to an uploaded file that can help during the import process.
For example a video or album name can be annotated so that after the upload was complete,
the file already has the correct name for importing.
Annotation is an object that can have multiple keys.
Args:
taskid (str): Unique ID to identify the upload task
annotations (dict): A dictionary with elements to annotate to an upload
Returns:
*Nothing*
"""
# Annotate upload
self.uploadmanager.AnnotateUpload(taskid, annotations)
return
[docs] def InitiateContentIntegration(self, taskid, musicpath):
"""
This method integrates uploaded content into the Music Directory.
The task with the ID ``taskid`` must be in state ``"readyforintegration"``.
When the content type is ``"artwork"``, then the artwork will directly be imported.
Then the ``musicpath`` determines the target of the artwork import.
Because artwork cannot only be integrated but must be imported,
the import process will directly be triggered.
Otherwise the ``musicpath`` defines the target file or directory inside the Music Directory
to that the uploaded content will be moved to.
Args:
taskid (str): ID to identify the upload
musicpath (str): Path to the destination directory or of the target for artwork import.
Returns:
The task ID as string
"""
return self.integrationmanager.InitiateIntegration(taskid, musicpath)
[docs] def InitiateMusicImport(self, contenttype, contentpath):
"""
This method uses :meth:`musicdb.mdbapi.importmanager.ImportManager.InitiateImport` to prepare and start an import task.
The ``contentpath`` addresses the content that shall be imported.
This path must be relative to the music directory.
Args:
contenttype (str): Type of the content: (``"video"``, ``"album"``, ``"artwork"``)
contentpath (str): Path to the content that shall be imported. For example to an Album.
Returns:
The task ID as string
Example:
.. code-block:: javascript
MusicDB_Request("InitiateMusicImport", "NewTaskID",
{contenttype: "album", contentpath: "artist/2021 - new album"});
// …
function onMusicDBMessage(fnc, sig, args, pass)
{
if(sig != NewTaskID)
return;
if(args == null)
console.error("Initiate Import failed!");
else
console.log(`New Task ID: ${args}`);
}
function onMuiscDBNotification(fnc, sig, rawdata)
{
if(fnc == "MusicDB:Task" && sig == "StateUpdate")
console.log(`State Update: ${rawdata["state"]}`);
}
"""
taskid = self.importmanager.InitiateImport(contenttype, contentpath);
return taskid
[docs] def InitiateArtworkImport(self, sourcepath, targetpath):
"""
Similar to :meth:`~InitiateMusicImport`, but uses
:meth:`musicdb.mdbapi.artworkmanager.ArtworkManager.InitiateImport` to prepare and start an import task.
The ``sourcepath`` must address an existing image file in the upload directory or a song file or video file in the music directory.
The ``targetpath`` must address a video file or album directory inside the music directory.
The target path must address music that is also registered in the database.
Args:
sourcepath (str): Path to an image, song or video file
targetpath (str): Path to an album directory or video file
Returns:
The task ID as string
"""
taskid = self.artworkmanager.InitiateImport(sourcepath, targetpath)
return taskid
[docs] def InitiateFilesystemScan(self):
"""
This method can be used to scan the file system and find lost paths inside the database.
It does the following steps:
#. Check all database entries if the associated files and directories can be found via :meth:`musicdb.mdbapi.music.MusicDBMusic.FindLostPaths`
#. If not, collect their database entry
#. Find all new files and directories via :meth:`musicdb.mdbapi.music.MusicDBMusic.FindNewPaths`
#. Collect their path and check sum (this may take some time)
The returned information has the following structure
* ``"newpaths"``
* ``"artists"``, ``"albums"``, ``"songs"``, ``"filteredsongs"``, ``"videos"``
* Each entry is a list of paths as string
* ``"lostpaths"``
* ``"artists"``, ``"albums"``, ``"songs"``, ``"videos"``
* Each entry is a list of database entries as dictionary
The found information are annotated to the task
Returns:
The task ID as string
Example:
.. code-block:: javascript
MusicDB_Call("InitiateFilesystemScan");
// …
function onMuiscDBNotification(fnc, sig, task)
{
if(fnc == "MusicDB:Task" && sig == "StateUpdate")
{
if(task["state"] == "fsscancomplete")
console.log(task["annotations"]);
}
}
"""
taskid = self.filesystemmanager.InitiateFilesystemScan()
return taskid
[docs] def RemoveUpload(self, taskid):
"""
This method triggers removing a specific upload.
This includes the uploaded file as well as the upload task information and annotations.
The upload task can be in any state.
When the remove-operation is triggered, its state gets changed to ``"remove"``.
See :meth:`musicdb.mdbapi.uploadmanager.UploadManager.RequestRemoveUpload` for details.
Args:
taskid (str): ID to identify the upload
"""
self.uploadmanager.RequestRemoveUpload(taskid)
return
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4