def Backup(self): """ Creates a backup of the configuration file. The path of the backup will be the same as the source file. It gets the following extension: ``.YYYY-MM-DDTHH:MM.bak`` Returns: *Nothing* """ backuppath = self.configpath backuppath += "." backuppath += datetime.now().isoformat(timespec='minutes') backuppath += ".bak" fs = Filesystem() try: fs.CopyFile(self.configpath, backuppath) except Exception as e: logging.warning( "creating backup of configuration failed with error %s. Writing backup to /tmp/configbackup.ini", str(e)) fs.CopyFile(self.configpath, "/tmp/configbackup.ini") return
def Backup(self): """ Creates a backup of the database file. The path of the backup will be the same as the source file. It gets the following extension: ``.YYYY-MM-DDTHH:MM.bak`` Returns: *Nothing* """ backuppath = self.databasepath backuppath += "." backuppath += datetime.now().isoformat(timespec='minutes') backuppath += ".bak" fs = Filesystem() fs.CopyFile(self.databasepath, backuppath) return
class MusicDBArtwork(object): """ Args: config: MusicDB configuration object database: MusicDB database Raises: TypeError: if config or database are not of the correct type ValueError: If one of the working-paths set in the config file does not exist """ def __init__(self, config, database): if type(config) != MusicDBConfig: raise TypeError("Config-class of unknown type") if type(database) != MusicDatabase: raise TypeError("Database-class of unknown type") self.db = database self.cfg = config self.fs = Filesystem() self.musicroot = Filesystem(self.cfg.music.path) self.artworkroot = Filesystem(self.cfg.artwork.path) # Define the prefix that must be used by the WebUI and server to access the artwork files # -> $PREFIX/$Artworkname.jpg self.manifestawprefix = "artwork" # Check if all paths exist that have to exist pathlist = [] pathlist.append(self.cfg.music.path) pathlist.append(self.cfg.artwork.path) pathlist.append(self.cfg.artwork.manifesttemplate) for path in pathlist: if not self.fs.Exists(path): raise ValueError("Path \"" + path + "\" does not exist.") # Instantiate dependent classes self.meta = MetaTags(self.cfg.music.path) self.awcache = ArtworkCache(self.cfg.artwork.path) def GetArtworkFromFile(self, album, tmpawfile): """ This method tries to get an artwork from the metadata of the first song of an album. With the first song, the first one in the database related to the album is meant. The metadata gets loaded and the artwork stored to a temporary file using the method :meth:`lib.metatags.MetaTags.StoreArtwork`. Args: album: Album entry from the MusicDB Database tmpawfile (str): Temporary artwork path (incl filename) to which the artwork shall be written Returns: ``True`` on success, otherwise ``False`` """ # Load the first files metadata songs = self.db.GetSongsByAlbumId(album["id"]) firstsong = songs[0] self.meta.Load(firstsong["path"]) retval = self.meta.StoreArtwork(tmpawfile) return retval def SetArtwork(self, albumid, artworkpath, artworkname): """ This method sets a new artwork for an album. It does the following things: #. Copy the artwork from *artworkpath* to the artwork root directory under the name *artworkname* #. Create scaled Versions of the artwork by calling :meth:`lib.cache.ArtworkCache.GetArtwork` for each resolution. #. Update entry in the database All new creates files ownership will be set to ``[music]->owner:[music]->group`` and gets the permission ``rw-rw-r--`` Args: albumid: ID of the Album that artwork shall be set artworkpath (str, NoneType): The absolute path of an artwork that shall be added to the database. If ``None`` the method assumes that the default artwork shall be set. *artworkname* will be ignored in this case. artworkname (str): The relative path of the final artwork. Returns: ``True`` on success, otherwise ``False`` Examples: .. code-block:: python # Copy from metadata extracted temporary artwork to the artwork directory self.SetArtwork(albumid, "/tmp/musicdbtmpartwork.jpg", "Artist - Album.jpg") # Copy a user-defined artwork to the artwork directory self.SetArtwork(albumid, "/home/username/downloads/fromzeintanetz.jpg", "Artist - Album.jpg") # Set the default artwork self.SetArtwork(albumid, None, any) """ if artworkpath: abssrcpath = self.fs.AbsolutePath(artworkpath) absdstpath = self.artworkroot.AbsolutePath(artworkname) # Copy file logging.debug("Copying file from \"%s\" to \"%s\"", abssrcpath, absdstpath) self.artworkroot.CopyFile(abssrcpath, absdstpath) # Set permissions to -rw-rw-r-- try: self.artworkroot.SetAttributes( artworkname, self.cfg.music.owner, self.cfg.music.group, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH) except Exception as e: logging.warning( "Setting artwork file attributes failed with error %s. \033[1;30m(Leaving them as they are)", str(e)) if not self.artworkroot.Exists(artworkname): logging.error( "Artwork \"%s\" does not exist but was expected to exist!", artworkname) return False # Scale file # convert edge-size to resolution # [10, 20, 30] -> ["10x10", "20x20", "30x30"] resolutions = [str(s) + "x" + str(s) for s in self.cfg.artwork.scales] for resolution in resolutions: relpath = self.awcache.GetArtwork(artworkname, resolution) if not self.artworkroot.Exists(relpath): logging.error( "Artwork \"%s\" does not exist but was expected to exist!", relpath) return False # Set permissions to -rw-rw-r-- try: self.artworkroot.SetAttributes( relpath, self.cfg.music.owner, self.cfg.music.group, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH) except Exception as e: logging.warning( "Setting artwork file attributes failed with error %s. \033[1;30m(Leaving them as they are)", str(e)) # Update database entry self.db.SetArtwork(albumid, artworkname) return True @staticmethod def CreateArtworkName(artistname, albumname): """ This method creates the name for an artwork regarding the following schema: ``$Artistname - $Albumname.jpg``. If there is a ``/`` in the name, it gets replaced by ``∕`` (U+2215, DIVISION SLASH) Args: artistname (str): Name of an artist albumname (str): Name of an album Returns: valid artwork filename """ artistname = artistname.replace("/", "∕") albumname = albumname.replace("/", "∕") imagename = artistname + " - " + albumname + ".jpg" return imagename def UpdateAlbumArtwork(self, album, artworkpath=None): """ This method updates the artwork path entry of an album and the artwork files in the artwork directory. If a specific artwork shall be forced to use, *artworkpath* can be set to this artwork file. Following the concept *The Filesystem Is Always Right* and *Do Not Trust Metadata*, the user specified artwork path has higher priority. Metadata will only be processed if *artworkpath* is ``None`` So an update takes place if *at least one* of the following condition is true: #. The database entry points to ``default.jpg`` #. *artworkpath* is not ``None`` #. If the database entry points to a nonexistent file Args: album: An Album Entry from the MusicDB Database artworkpath (str, NoneType): Absolute path of an artwork that shall be used as album artwork. If ``None`` the Method tries to extract the artwork from the meta data of an albums song. Returns: ``True`` If either the update was successful or there was no update necessary. ``False`` If the update failed. Reasons can be an invalid *artworkpath*-Argument """ # Create relative artwork path artist = self.db.GetArtistById(album["artistid"]) imagename = self.CreateArtworkName(artist["name"], album["name"]) # Check if there is no update necessary dbentry = album["artworkpath"] if dbentry != "default.jpg" and artworkpath == None: if self.artworkroot.IsFile( dbentry ): # If the file does not extist, it must be updated! return True # Check if the user given artworkpath is valid if artworkpath and not self.fs.IsFile(artworkpath): logging.error( "The artworkpath that shall be forces is invalid (\"%s\")! \033[1;30m(Artwork update will be canceled)", str(artworkpath)) return False # If there is no suggested artwork, try to get one from the meta data # In case this failes, use the default artwork if not artworkpath: artworkpath = "/tmp/musicdbtmpartwork.jpg" # FIXME: Hardcoded usually sucks retval = self.GetArtworkFromFile(album, artworkpath) if not retval: imagename = "default.jpg" artworkpath = None # Set new artwork logging.info("Updating artwork for album \"%s\" to \"%s\" at \"%s\".", album["name"], imagename, artworkpath) retval = self.SetArtwork(album["id"], artworkpath, imagename) return retval def GenerateAppCacheManifest(self): """ This method creates a manifest file for web browsers. Creating is done in two steps. First the template given in the configuration gets copied. Second the paths of all artworks get append to the file. Also, those of the scaled versions (as given in the config file). Returns: *Nothing* Raises: PermissonError: When there is no write access to the manifest file """ # copy manifest template template = open(self.cfg.artwork.manifesttemplate, "r") manifest = open(self.cfg.artwork.manifest, "w") for line in template: manifest.write(line) template.close() # and append all artworkd albums = self.db.GetAllAlbums() awpaths = [album["artworkpath"] for album in albums] resolutions = [str(s) + "x" + str(s) for s in self.cfg.artwork.scales] resolutions.append(".") for resolution in resolutions: for awpath in awpaths: path = os.path.join(self.manifestawprefix, resolution) path = os.path.join(path, awpath) manifest.write(path + "\n") manifest.close()
class UploadManager(object): """ This class manages uploading content to the server MusicDB runs on. All data is stored in the uploads-directory configured in the MusicDB configuration. Args: config: :class:`~lib.cfg.musicdb.MusicDBConfig` object holding the MusicDB Configuration database: (optional) A :class:`~lib.db.musicdb.MusicDatabase` instance Raises: TypeError: When the arguments are not of the correct type. """ def __init__(self, config, database): if type(config) != MusicDBConfig: raise TypeError("config argument not of type MusicDBConfig") if database != None and type(database) != MusicDatabase: raise TypeError( "database argument not of type MusicDatabase or None") self.db = database self.cfg = config self.uploadfs = Filesystem(self.cfg.uploads.path) self.musicfs = Filesystem(self.cfg.music.path) self.artworkfs = Filesystem(self.cfg.artwork.path) # TODO: check write permission of all directories self.fileprocessing = Fileprocessing(self.cfg.uploads.path) self.dbmanager = MusicDBDatabase(config, database) global Tasks if Tasks == None: self.LoadTasks() ##################################################################### # Callback Function Management # ##################################################################### def RegisterCallback(self, function): """ Register a callback function that reacts on Upload related events. For more details see the module description at the top of this document. Args: function: A function that shall be called on an event. Returns: *Nothing* """ global Callbacks Callbacks.append(function) def RemoveCallback(self, function): """ Removes a function from the list of callback functions. Args: function: A function that shall be called removed. Returns: *Nothing* """ global Callbacks # Not registered? Then do nothing. if not function in Callbacks: logging.warning( "A Streaming Thread callback function should be removed, but did not exist in the list of callback functions!" ) return Callbacks.remove(function) def NotifyClient(self, notification, task, message=None): """ This method triggers a client-notification. There are three kind of notifications: * ``"ChunkRequest"``: A new chunk of data is requested * ``"StateUpdate"``: The state or annotations of an upload-task has been changed. See ``"state"`` value. * ``"InternalError"``: There is an internal error occurred during. See ``"message"`` value. The notification comes with the current status of the upload process. This includes the following keys - independent of the state of the upload: * uploadid: ID of the upload the notification is associated with * offset: Offset of the requested data in the source file * chunksize: The maximum chunk size * state: The current state of the upload task * message: ``null``/``None`` or a message from the server * uploadtask: The task dictionary itself * uploadslist: Except for ``ChunkRequest`` events, the WebSocket server append the result of :meth:`lib.ws.mdbwsi.MusicDBWebSocketInterface.GetUploads` to the notification *task* can be ``None`` in case the notification is meant to be an information that a given upload ID is invalid. Args: notification (str): Name of the notification task (dict): Task structure message (str): (optional) text message (like an error message) to the client Returns: *Nothing* Raises: ValueError: When notification has an unknown notification name """ if not notification in [ "ChunkRequest", "StateUpdate", "InternalError" ]: raise ValueError("Unknown notification \"%s\"" % (notification)) status = {} if task != None: status["uploadid"] = task["id"] status["offset"] = task["offset"] # offset of the data to request status[ "chunksize"] = 4096 * 100 # Upload 400KiB (TODO: Make configurable) status["state"] = task["state"] status["uploadtask"] = task else: status["uploadid"] = None status["offset"] = None status["chunksize"] = None status["state"] = "notexisting" status["uploadtask"] = None status["message"] = message global Callbacks for callback in Callbacks: try: callback(notification, status) except Exception as e: logging.exception( "A Upload Management event callback function crashed!") ##################################################################### # State management # ##################################################################### def SaveTask(self, task): """ This method saves a task in the uploads directory under ``tasks/${Task ID}.json`` Args: task (dict): The task to save Returns: *Nothing* """ taskid = task["id"] data = json.dumps(task) path = self.cfg.uploads.path + "/tasks/" + taskid + ".json" if not self.uploadfs.IsDirectory("tasks"): logging.debug("tasks directory missing. Creating \"%s\"", self.cfg.uploads.path + "/tasks") self.uploadfs.CreateSubdirectory("tasks") with open(path, "w+") as fd: fd.write(data) return def LoadTasks(self): """ Loads all task from the JSON files inside the tasks-directory. The list of active tasks will be replaced by the loaded tasks. Returns: *Nothing* """ logging.debug("Loading Upload-Tasks…") taskfilenames = self.uploadfs.ListDirectory("tasks") global Tasks Tasks = {} for taskfilename in taskfilenames: taskpath = self.cfg.uploads.path + "/tasks/" + taskfilename if self.uploadfs.GetFileExtension(taskpath) != "json": continue try: with open(taskpath) as fd: task = json.load(fd) except Exception as e: logging.warning( "Loading task file \"%s\" failed with error \"%s\". \033[1;30m(File will be ignored)", str(taskpath), str(e)) continue if "id" not in task: logging.warning( "File \"%s\" is not a valid task (ID missing). \033[1;30m(File will be ignored)", str(taskpath), str(e)) continue Tasks[task["id"]] = task return ##################################################################### # Management Functions # ##################################################################### def InitiateUpload(self, uploadid, mimetype, contenttype, filesize, checksum, sourcefilename): """ Initiates an upload of a file into a MusicDB managed file space. After calling this method, a notification gets triggered to request the first chunk of data from the clients. In case uploads are deactivated in the MusicDB Configuration, an ``"InternalError"`` Notification gets sent to the clients. Args: uploadid (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"``) Raises: TypeError: When one of the arguments has not the expected type ValueError: When *contenttype* does not have the expected values """ if type(uploadid) != str: raise TypeError("Upload ID must be of type string") if type(mimetype) != str: raise TypeError("mime type must be of type string") if type(contenttype) != str: raise TypeError("content type must be of type string") if contenttype not in ["video", "album", "artwork"]: raise ValueError( "content type \"%s\" not valid. \"video\", \"album\" or \"artwork\" expected." % (str(contenttype))) if type(filesize) != int: raise TypeError("file size must be of type int") if filesize <= 0: raise ValueError("file size must be greater than 0") if type(checksum) != str: raise TypeError("Checksum must be of type string") if type(sourcefilename) != str: raise TypeError("Source file name must be of type string") if not self.cfg.uploads.allow: self.NotifyClient("InternalError", None, "Uploads deactivated") logging.warning( "Uploads not allowed! \033[1;30m(See MusicDB Configuration: [uploads]->allow)" ) return fileextension = self.uploadfs.GetFileExtension(sourcefilename) destinationname = contenttype + "-" + checksum + "." + fileextension destinationpath = self.cfg.uploads.path + "/" + destinationname # TODO: Check if there is already a task with the given ID. # If this task is in waitforchunk state, the upload can be continued instead of restarting it. # Remove existing upload if destination path exists self.uploadfs.RemoveFile( destinationpath) # Removes file when it exists # Create File with open(destinationpath, "w+b"): pass task = {} task["id"] = uploadid task["filesize"] = filesize task["offset"] = 0 task["contenttype"] = contenttype task["mimetype"] = mimetype task["sourcefilename"] = sourcefilename task["sourcechecksum"] = checksum task["destinationpath"] = destinationpath task[ "videofile"] = None # Path to the video file in the music directory task["state"] = "waitforchunk" task["annotations"] = {} self.SaveTask(task) global Tasks Tasks[uploadid] = task self.NotifyClient("ChunkRequest", task) return def RequestRemoveUpload(self, uploadid): """ 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"``. Only the ``"remove"`` state gets set. Removing will be done by the Management Thread. Args: uploadid (str): ID of the upload-task Returns: ``True`` on success """ try: task = self.GetTaskByID(uploadid) except Exception as e: logging.error( "Internal error while requesting a new chunk of data: %s", str(e)) return False self.UpdateTaskState(task, "remove") return True def GetTaskByID(self, uploadid): """ This method returns an existing task from the tasklist. The task gets identified by its ID aka Upload ID When the task does not exits, the clients get an ``"InternalError"`` notification. The tasks state is then ``"notexisting"``. Args: uploadid (str): ID of the upload-task Returns: A task dictionary Raises: TypeError: When *uploadid* is not a string ValueError: When *uploadid* is not a valid key in the Tasks-dictionary """ if type(uploadid) != str: raise TypeError("Upload ID must be a string. Type was \"%s\"." % (str(type(uploadid)))) global Tasks if uploadid not in Tasks: self.NotifiyClient("InternalError", None, "Invalid Upload ID") raise ValueError("Upload ID \"%s\" not in Task Queue.", str(uploadid)) return Tasks[uploadid] def UpdateTaskState(self, task, state, errormessage=None): """ This method updates and saves the state of an task. An ``"StateUpdate"`` notification gets send as well. If *errormessage* is not ``None``, the notification gets send as ``"InternalError"`` with the message Args: task (dict): Task object to update state (str): New state message (str): Optional message Returns: *Nothing* """ task["state"] = state self.SaveTask(task) if errormessage: self.NotifyClient("InternalError", task, errormessage) else: self.NotifyClient("StateUpdate", task) return def NewChunk(self, uploadid, rawdata): """ This method processes a new chunk received from the uploading client. Args: uploadid (str): Unique ID to identify the upload task rawdata (bytes): Raw data to append to the uploaded data Returns: ``False`` in case an error occurs. Otherwise ``True``. Raises: TypeError: When *rawdata* is not of type ``bytes`` """ if type(rawdata) != bytes: raise TypeError("raw data must be of type bytes. Type was \"%s\"" % (str(type(rawdata)))) try: task = self.GetTaskByID(uploadid) except Exception as e: logging.error( "Internal error while requesting a new chunk of data: %s", str(e)) return False chunksize = len(rawdata) filepath = task["destinationpath"] try: with open(filepath, "ab") as fd: fd.write(rawdata) except Exception as e: logging.warning( "Writing chunk of uploaded data into \"%s\" failed: %s \033[1;30m(Upload canceled)", filepath, str(e)) self.UpdateTaskState( task, "uploadfailed", "Writing data failed with error: \"%s\"" % (str(e))) return False task["offset"] += chunksize self.SaveTask(task) if task["offset"] >= task["filesize"]: # Upload complete self.UploadCompleted(task) else: # Get next chunk of data self.NotifyClient("ChunkRequest", task) return True def UploadCompleted(self, task): """ This method continues the file management after an upload was completed. The following tasks were performed: * Checking the checksum of the destination file (SHA1) and compares it with the ``"sourcechecksum"`` from the *task*-dict. When the upload was successful, it notifies the clients with a ``"UploadComplete"`` notification. Otherwise with a ``"UploadFailed"`` one. Args: task (dict): The task that upload was completed Returns: ``True`` When the upload was successfully complete, otherwise ``False`` """ # Check checksum destchecksum = self.fileprocessing.Checksum(task["destinationpath"], "sha1") if destchecksum != task["sourcechecksum"]: logging.error( "Upload Failed: \033[0;36m%s \e[1;30m(Checksum mismatch)", task["destinationpath"]) self.UpdateTaskState(task, "uploadfailed", "Checksum mismatch") return False logging.info("Upload Complete: \033[0;36m%s", task["destinationpath"]) self.UpdateTaskState(task, "uploadcomplete") # Now, the Management Thread takes care about post processing or removing no longer needed content return True def GetTasks(self): """ Returns: The dictionary with all upload tasks """ global Tasks return Tasks def PreProcessUploadedFile(self, task): """ This method initiates pre-processing of an uploaded file. Depending on the *contenttype* different post processing methods are called: * ``"video"``: :meth:`~PreProcessVideo` * ``"artwork"``: :meth:`~PreProcessArtwork` The task must be in ``"uploadcomplete"`` state, otherwise nothing happens but printing an error message. If post processing was successful, the task state gets updated to ``"preprocessed"``. When an error occurred, the state will become ``"invalidcontent"``. Args: task (dict): the task object of an upload-task Returns: ``True`` on success, otherwise ``False`` """ if task["state"] != "uploadcomplete": logging.error( "task must be in \"uploadcomplete\" state for post processing. Actual state was \"%s\". \033[1;30m(Such a mistake should not happen. Anyway, the task won\'t be post process and nothing bad will happen.)", task["state"]) return False # Perform post processing logging.debug("Preprocessing upload %s -> %s", str(task["sourcefilename"]), str(task["destinationpath"])) success = False if task["contenttype"] == "video": success = self.PreProcessVideo(task) elif task["contenttype"] == "artwork": success = self.PreProcessArtwork(task) else: logging.warning( "Unsupported content type of upload: \"%s\" \033[1;30m(Upload will be ignored)", str(task["contenttype"])) self.UpdateTaskState(task, "invalidcontent", "Unsupported content type") return False # Update task state if success == True: newstate = "preprocessed" else: newstate = "invalidcontent" self.UpdateTaskState(task, newstate) return success def PreProcessVideo(self, task): """ Args: task (dict): the task object of an upload-task """ meta = MetaTags() try: meta.Load(task["destinationpath"]) except ValueError: logging.error( "The file \"%s\" uploaded as video to %s is not a valid video or the file format is not supported. \033[1;30m(File will be not further processed.)", task["sourcefilename"], task["destinationpath"]) return False # Get all meta infos (for videos, this does not include any interesting information. # Maybe the only useful part is the Load-method to check if the file is supported by MusicDB #tags = meta.GetAllMetadata() #logging.debug(tags) return True def PreProcessArtwork(self, task): """ Args: task (dict): the task object of an upload-task """ origfile = task["destinationpath"] extension = self.uploadfs.GetFileExtension(origfile) jpegfile = origfile[:-len(extension)] + "jpg" if extension != "jpg": logging.debug( "Transcoding artwork file form %s (\"%s\") to JPEG (\"%s\")", extension, origfile, jpegfile) im = Image.open(origfile) im = im.convert("RGB") im.save(jpegfile, "JPEG", optimize=True, progressive=True) task["artworkfile"] = jpegfile return True def AnnotateUpload(self, uploadid, annotations): """ This method can be used to add additional information to an upload. This can be done during or after the upload process. Args: uploadid (str): ID to identify the upload Returns: ``True`` on success, otherwise ``False`` Raises: TypeError: When *uploadid* is not of type ``str`` ValueError: When *uploadid* is not included in the Task Queue """ try: task = self.GetTaskByID(uploadid) except Exception as e: logging.error( "Internal error while requesting a new chunk of data: %s", str(e)) return False for key, item in annotations.items(): task["annotations"][key] = item self.SaveTask(task) self.NotifyClient("StateUpdate", task) return True def IntegrateUploadedFile(self, uploadid, triggerimport): """ This method integrated the uploaded files into the music directory. The whole file tree will be created following the MusicDB naming scheme. The upload task must be in ``preprocesses`` state. If not, nothing happens. When *triggerimport* is ``true``, the upload manager start importing the music. This happens asynchronously inside the Upload Manager Thread. Args: uploadid (str): ID to identify the upload Returns: ``True`` on success, otherwise ``False`` Raises: TypeError: When *uploadid* is not of type ``str`` ValueError: When *uploadid* is not included in the Task Queue """ try: task = self.GetTaskByID(uploadid) except Exception as e: logging.error( "Internal error while requesting a new chunk of data: %s", str(e)) return False if task["state"] != "preprocessed": logging.warning( "Cannot integrate an upload that is not in \"preprocessed\" state. Upload with ID \"%s\" was in \"%s\" state! \033[1;30m(Nothing will be done)", str(task["id"]), str(task["state"])) return # Perform post processing success = False if task["contenttype"] == "video": success = self.IntegrateVideo(task) elif task["contenttype"] == "artwork": success = True # Importing artwork does not require the file at any specific place else: logging.warning( "Unsupported content type of upload: \"%s\" \033[1;30m(Upload will be ignored)", str(task["contenttype"])) self.UpdateTaskState(task, "integrationfailed", "Unsupported content type") return # Update task state if success == True: newstate = "integrated" else: newstate = "integrationfailed" self.UpdateTaskState(task, newstate) # Trigger import if success == False or triggerimport == False: return # … but only if wanted, and previous step was successful self.UpdateTaskState( task, "startimport") # The upload management thread will do the rest return def IntegrateVideo(self, task): """ When an annotation needed for creating the video file path in the music directory is missing, ``False`` gets returned and an error message written into the log """ uploadedfile = task["destinationpath"] # uploaded file try: artistname = task["annotations"]["artistname"] releasedate = task["annotations"]["release"] videoname = task["annotations"]["name"] except KeyError as e: logging.error( "Collection video information for creating its path name failed with key-error for: %s \033[1;30m(Make sure all important annotations are given to that upload: name, artistname, release)", str(e)) return False fileextension = self.uploadfs.GetFileExtension(uploadedfile) videofile = artistname + "/" + releasedate + " - " + videoname + "." + fileextension task["videofile"] = videofile logging.debug("Integrating upload %s -> %s", str(uploadedfile), str(videofile)) # Check if video file already exists if self.musicfs.Exists(videofile): logging.warning( "File \"%s\" already exists in the music directory! \033[1;30m(It will NOT be overwritten)", str(videofile)) self.NotifyClient("InternalError", task, "File already exists in the music directory") return False # Check if artist directory exists if not self.musicfs.IsDirectory(artistname): logging.info( "Artist directory for \"%s\" does not exist and will be created.", str(artistname)) try: self.musicfs.CreateSubdirectory(artistname) except PermissionError: logging.error( "Creating artist sub-directory \"%s\" failed! - Permission denied! \033[1;30m(MusicDB requires write permission to the music file tree)", str(artistname)) self.NotifyClient( "InternalError", task, "Creating artist directory failed - Permission denied") return False # Copy file, create Artist directory if not existing try: success = self.musicfs.CopyFile(uploadedfile, videofile) except PermissionError: logging.error( "Copying video file to \"%s\" failed! - Permission denied! \033[1;30m(MusicDB requires write permission to the music file tree)", str(videofile)) self.NotifyClient("InternalError", task, "Copying failed - Permission denied") return False if (success): logging.info("New video file at %s", str(videofile)) else: logging.warning("Copying video file to \"%s\" failed!", str(videofile)) return success def ImportVideo(self, task): """ Task state must be ``"startimport"`` and content type must be ``"video"`` Returns: ``True`` on success. """ # Check task state and type if task["state"] != "startimport": logging.warning( "Cannot import an upload that is not in \"startimport\" state. Upload with ID \"%s\" was in \"%s\" state! \033[1;30m(Nothing will be done)", str(task["id"]), str(task["state"])) return False success = False if task["contenttype"] != "video": logging.warning( "Video expected. Actual type of upload: \"%s\" \033[1;30m(No video will be imported)", str(task["contenttype"])) return False # Get important information try: artistname = task["annotations"]["artistname"] videopath = task["videofile"] except KeyError as e: logging.error( "Collecting video information for importing failed with key-error for: %s \033[1;30m(Make sure the artist name is annotated to the upload)", str(e)) return False # Check if the artist already exists in the database - if not, add it artist = self.db.GetArtistByPath(artistname) if artist == None: logging.info("Importing new artist: \"%s\"", artistname) try: self.dbmanager.AddArtist(artistname) except Exception as e: logging.error( "Importing artist \"%s\" failed with error: %s \033[1;30m(Video upload canceled)", str(artistname), str(e)) self.NotifyClient("InternalError", task, "Importing artist failed") return False artist = self.db.GetArtistByPath(artistname) # Import video try: success = self.dbmanager.AddVideo(videopath, artist["id"]) except Exception as e: logging.error( "Importing video \"%s\" failed with error: %s \033[1;30m(Video upload canceled)", str(videopath), str(e)) self.NotifyClient("InternalError", task, "Importing video failed") return False if not success: logging.error( "Importing video \"%s\" failed. \033[1;30m(Video upload canceled)", str(videopath), str(e)) self.NotifyClient("InternalError", task, "Importing video failed") return False # Add origin information to database if annotated try: origin = task["annotations"]["origin"] except KeyError as e: pass else: video = self.db.GetVideoByPath(videopath) video["origin"] = origin self.db.WriteVideo(video) logging.info("Importing Video succeeded") return True def ImportVideoArtwork(self, task): """ Returns: ``True`` on success """ # Check task state and type if task["state"] != "importartwork": logging.warning( "Cannot import artwork that is not in \"importartwork\" state. Upload with ID \"%s\" was in \"%s\" state! \033[1;30m(Nothing will be done)", str(task["id"]), str(task["state"])) return False if task["contenttype"] != "video": logging.warning( "Video expected. Actual type of upload: \"%s\" \033[1;30m(No video will be imported)", str(task["contenttype"])) return False # Start generating the artworks videopath = task["videofile"] framemanager = VideoFrames(self.cfg, self.db) video = self.db.GetVideoByPath(videopath) if not video: logging.error( "Getting video \"%s\" from database failed. \033[1;30m(Artwork import canceled)", str(videopath), str(e)) self.NotifyClient("InternalError", task, "Video artwork import failed") return False retval = framemanager.UpdateVideoFrames(video) if retval == False: logging.error( "Generating video frames and preview failed for video \"%s\". \033[1;30m(Artwork import canceled)", str(videopath)) self.NotifyClient("InternalError", task, "Video artwork import failed") return False logging.info("Importing Video thumbnails and previews succeeded") return True def ImportArtwork(self, task): """ Task state must be ``"startimport"`` and content type must be ``"artwork"`` Returns: ``True`` on success. """ # Check task state and type if task["state"] != "startimport": logging.warning( "Cannot import an upload that is not in \"startimport\" state. Upload with ID \"%s\" was in \"%s\" state! \033[1;30m(Nothing will be done)", str(task["id"]), str(task["state"])) return False success = False if task["contenttype"] != "artwork": logging.warning( "Album artwork expected. Actual type of upload: \"%s\" \033[1;30m(No artwork will be imported)", str(task["contenttype"])) return False # Get important information try: artistname = task["annotations"]["artistname"] albumname = task["annotations"]["albumname"] albumid = task["annotations"]["albumid"] sourcepath = task["artworkfile"] except KeyError as e: logging.error( "Collecting artwork information for importing failed with key-error for: %s \033[1;30m(Make sure the artist and album name is annotated as well as the album ID.)", str(e)) return False # Import artwork awmanager = MusicDBArtwork(self.cfg, self.db) artworkname = awmanager.CreateArtworkName(artistname, albumname) success = awmanager.SetArtwork(albumid, sourcepath, artworkname) if not success: logging.error( "Importing artwork \"%s\" failed. \033[1;30m(Artwork upload canceled)", str(sourcepath)) self.NotifyClient("InternalError", task, "Importing artwork failed") return False # Add origin information to database if annotated try: origin = task["annotations"]["origin"] except KeyError as e: pass else: video = self.db.GetVideoByPath(videopath) video["origin"] = origin self.db.WriteVideo(video) logging.info("Importing Artwork succeeded") return True def RemoveTask(self, task): """ ``tasks/${Task ID}.json`` """ logging.info( "Removing uploaded \"%s\" file and task \"%s\" information.", task["sourcefilename"], task["id"]) datapath = task["destinationpath"] taskpath = "tasks/" + task["id"] + ".json" # if artwork, remove artworkfile as well if task["contenttype"] == "artwork": artworkfile = task["artworkfile"] logging.debug("Removing %s", self.uploadfs.AbsolutePath(artworkfile)) self.uploadfs.RemoveFile(artworkfile) logging.debug("Removing %s", self.uploadfs.AbsolutePath(datapath)) self.uploadfs.RemoveFile(datapath) logging.debug("Removing %s", self.uploadfs.AbsolutePath(taskpath)) self.uploadfs.RemoveFile(taskpath) return True