Source code for musicdb.lib.ws.mdbwsi

# 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 GetTags(self): """ This method returns all tags that are available, separated by the tag classes. Returns: A dictionary of all classes with lists of all available tags Example: .. code-block:: javascript MusicDB_Request("GetTags", "ShowTags"); // … function onMusicDBMessage(fnc, sig, args, pass) { if(fnc == "GetTags" && sig == "ShowTags") { let genres, subgenres, moods; genres = args.genres; subgenres = args.subgenres; moods = args.moods; for(let tag of genres) console.log(tag.name); for(let tag of subgenres) console.log(tag.name); for(let tag of moods) console.log(tag.name); } } """ tags = self.database.GetAllTags() genres, subgenres, moods = self.database.SplitTagsByClass(tags) tags = {} tags["genres"] = genres tags["subgenres"] = subgenres tags["moods"] = moods return tags
[docs] def GetTagsStatistics(self): """ This method returns the usage statistics as a dictionary with an entry for each tag. The key is the tag ID. The value is another dictionary with the following keys: * **tag:** The whole tag entry for the corresponding ID * **songs:** The amount of songs tagged with this tag as integer * **albums:** The amount of albums tagged with this tag as integer * **videos:** The amount of videos tagged with this tag as integer * **children:** Number of child tags as integer The level of confidence or if the tag was approved for the song/video/album is not considered. All set tags are counted. Returns: A dictionary with usage-statistics of all tags Example: .. code-block:: javascript MusicDB_Request("GetTagsStatictics", "ShowTags"); // … function onMusicDBMessage(fnc, sig, args, pass) { if(fnc == "GetTagsStatistics" && sig == "ShowTags") { for(let entry of args) console.log(`The tag ${entry.tag.name} is assigned to ${entry.songs} songs.`); } } """ tags = self.database.GetAllTags() retval = {} for tag in tags: tagid = tag["id"] key = str(tagid) songs, albums, videos, children = self.database.GetTagStatistics(tagid) retval[key] = {} retval[key]["tag"] = tag; retval[key]["songs"] = songs; retval[key]["albums"] = albums; retval[key]["videos"] = videos; retval[key]["children"] = children; return retval
[docs] def GetSongTags(self, songid): """ Returns a dictionary with the following keys: * **songid:** The same ID as given as argument * **genres:** A list of tags of class *Genre* * **subgenres:** A list of tags of class *Subgenre* * **moods:** A list of tags of class *Mood* Each entry of the dict is a list with tags. Each tag is a dictionary as returned by :meth:`musicdb.lib.db.musicdb.MusicDatabase.GetTargetTags`. Args: songid (int): ID of the song for which the tags are requested Returns: A dict of tags sorted by classes Example: .. code-block:: javascript MusicDB_Request("GetSongTags", "ShowSongTags"); // … function onMusicDBMessage(fnc, sig, args, pass) { if(fnc == "GetSongTags" && sig == "ShowSongTags") { let genres, subgenres, moods; genres = args.genres; subgenres = args.subgenres; moods = args.moods; console.log("Tags of the song with the ID " + args.songid); for(let tag of genres) console.log(tag.name); for(let tag of subgenres) console.log(tag.name); for(let tag of moods) console.log(tag.name); } } """ tags = self.database.GetTargetTags("song", songid) genres, subgenres, moods = self.database.SplitTagsByClass(tags) tags = {} tags["songid"] = songid # this is necessary to not loose context tags["genres"] = genres tags["subgenres"] = subgenres tags["moods"] = moods return tags
[docs] def GetAlbumTags(self, albumid): """ Similar to :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSongTags`. This method returns the tags for an Album. """ tags = self.database.GetTargetTags("album", albumid) genres, subgenres, moods = self.database.SplitTagsByClass(tags) tags = {} tags["albumid"] = albumid # this is necessary to not loose context tags["genres"] = genres tags["subgenres"] = subgenres tags["moods"] = moods return tags
[docs] def GetVideoTags(self, videoid): """ Similar to :meth:`~musicdb.lib.ws.mdbwsi.MusicDBWebSocketInterface.GetSongTags`. This method returns the tags for a Video. """ tags = self.database.GetTargetTags("video", videoid) genres, subgenres, moods = self.database.SplitTagsByClass(tags) tags = {} tags["videoid"] = videoid # this is necessary to not loose context tags["genres"] = genres tags["subgenres"] = subgenres tags["moods"] = moods return tags
[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