Source code for musicdb.mdbapi.musicdirectory

# 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/>.
"""
This module provides a set of methods to manage the Music Directory.
The :class:`~MusicDirectory` is derived from :class:`musicdb.lib.filesystem.Filesystem`.
"""

import os
import shutil
import logging
import subprocess
from pathlib import Path
from typing  import Union, Optional
from musicdb.lib.fileprocessing import Fileprocessing
from musicdb.mdbapi.accesspermissions   import AccessPermissions


[docs]class MusicDirectory(Fileprocessing): """ This class provides an interface to the Music Directory. The whole class assumes that it is used with an Unicode capable UNIX-style filesystem. It is derived from :class:`musicdb.lib.fileprocessing.Fileprocessing`. In comparison to the :class:`~musicdb.mdbapi.music.MusicDBMusic`, only the files are on focus, not the music database. Keep in mind that applying some of the methods of this class can harm the connection between the database entries and their associated files. Args: config: MusicDB configuration object """ def __init__(self, config): Fileprocessing.__init__(self, config.directories.music) self.cfg = config self.ap = AccessPermissions(self.cfg, self.cfg.directories.music) # read lists with files and directories that shall be ignored by the scanner self.ignoreartists = self.cfg.music.ignoreartists self.ignorealbums = self.cfg.music.ignorealbums self.ignoresongs = self.cfg.music.ignoresongs
[docs] def RenameSongFile(self, oldpath, newpath): """ Renames a song inside the Music Directory. In general it is not checked if the new path fulfills the Music Naming Scheme (See :doc:`/usage/music`). The position of the file should be plausible anyway. So a song file should be inside a artist/album/-directory. The complete path, relative to the Music Directory must be given. 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:: Python // Will succeed oldpath = "Artist/2021 - Album Name/01 old file name.mp3" newpath = "Artist/2021 - Album Name/01 new file name.mp3" musicdirectory.RenameSongFile(oldpath, newpath) // Will succeed even when the song name does not fulfill the naming scheme for song files oldpath = "Artist/2021 - Album Name/old file name.mp3" newpath = "Artist/2021 - Album Name/new file name.mp3" musicdirectory.RenameSongFile(oldpath, newpath) // Will fail if because album name changed as well oldpath = "Artist/Old Album Name/Old Song Name.flac", newpath = "Artist/2021 - New Album Name/01 New Song Name.flac" musicdirectory.RenameSongFile(oldpath, newpath) """ # Check if old path is a valid path to a file in the Music Directory if not self.IsFile(oldpath): logging.warning("Rename Music Path failed because old path \"%s\" does not exists inside the Music Directory", str(oldpath)) return False # Check if new path addresses an already existing file if self.Exists(newpath): logging.warning("Rename Music Path failed because new path \"%s\" does already exist inside the Music Directory", str(newpath)) return False # Check if path if path is plausible oldcontenttype = self.EstimateContentTypeByPath(oldpath) newcontenttype = self.EstimateContentTypeByPath(newpath) if oldcontenttype != "song": logging.warning("Old path (\"%s\") does not address a song. It was estimated as: %s \033[1;30m(Old path will not be renamed)", oldpath, oldcontenttype) return False if newcontenttype != "song": logging.warning("New path (\"%s\") does not address a song. It was estimated as: %s \033[1;30m(Old path will not be renamed)", newpath, newcontenttype) return False # Rename path logging.info("Renaming \033[0;36m%s\033[1;34m ➜ \033[0;36m%s", oldpath, newpath) try: success = self.Rename(oldpath, newpath) except Exception as e: logging.error("Renaming \"%s\" to \"%s\" failed with exception: %s \033[1;30m(Nothing changed, old path is still valid)", oldpath, newpath, str(e)) success = False if not success: logging.warning("Renaming \"%s\" to \"%s\" failed. \033[1;30m(Nothing changed, old path is still valid)", oldpath, newpath) return False return True
[docs] def RenameVideoFile(self, oldpath, newpath): """ Renames a video inside the Music Directory. In general it is not checked if the new path fulfills the Music Naming Scheme (See :doc:`/usage/music`). The position of the file should be plausible anyway. So a song file should be inside a artist-directory. The complete path, relative to the Music Directory must be given. 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:: Python // Will succeed oldpath = "Artist/2021 - old file name.m4v" newpath = "Artist/2021 - new file name.m4v" musicdirectory.RenameVideoFile(oldpath, newpath) // Will succeed even when the video name does not fulfill the naming scheme for video files oldpath = "Artist/old file name.m4v" newpath = "Artist/new file name.m4v" musicdirectory.RenameVideoFile(oldpath, newpath) // Will fail if because artist name changed as well oldpath = "Artist/Old Video Name.m4v", newpath = "Different Artist/2020 - New Video Name.m4v" musicdirectory.RenameVideoFile(oldpath, newpath) """ # Check if old path is a valid path to a file in the Music Directory if not self.IsFile(oldpath): logging.warning("Rename Music Path failed because old path \"%s\" does not exists inside the Music Directory", str(oldpath)) return False # Check if new path addresses an already existing file if self.Exists(newpath): logging.warning("Rename Music Path failed because new path \"%s\" does already exist inside the Music Directory", str(newpath)) return False # Check if path if path is plausible oldcontenttype = self.EstimateContentTypeByPath(oldpath) newcontenttype = self.EstimateContentTypeByPath(newpath) if oldcontenttype != "video": logging.warning("Old path (\"%s\") does not address a video. It was estimated as: %s \033[1;30m(Old path will not be renamed)", oldpath, oldcontenttype) return False if newcontenttype != "video": logging.warning("New path (\"%s\") does not address a video. It was estimated as: %s \033[1;30m(Old path will not be renamed)", newpath, newcontenttype) return False # Rename path logging.info("Renaming \033[0;36m%s\033[1;34m ➜ \033[0;36m%s", oldpath, newpath) try: success = self.Rename(oldpath, newpath) except Exception as e: logging.error("Renaming \"%s\" to \"%s\" failed with exception: %s \033[1;30m(Nothing changed, old path is still valid)", oldpath, newpath, str(e)) success = False if not success: logging.warning("Renaming \"%s\" to \"%s\" failed. \033[1;30m(Nothing changed, old path is still valid)", oldpath, newpath) return False 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`). 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 oldpath = "Artist/2021 - Old Album Name" newpath = "Artist/2021 - New Album Name" musicdirectory.RenameAlbumDirectory(oldpath, newpath) // Will succeed even if the album name does not fulfill the naming requirements oldpath = "Artist/Old Album Name" newpath = "Artist/New Album Name" musicdirectory.RenameAlbumDirectory(oldpath, newpath) // Will fail because artist name changed as well. oldpath = "Artist/Old Album Name" newpath = "New Artist/2021 - New Album Name" musicdirectory.RenameAlbumDirectory(oldpath, newpath) """ # Check if old path is a valid path to a file in the Music Directory if not self.IsDirectory(oldpath): logging.warning("Rename Album Path failed because old path \"%s\" does not exists inside the Music Directory", str(oldpath)) return False # Check if new path addresses an already existing file if self.Exists(newpath): logging.warning("Rename Album Path failed because new path \"%s\" does already exist inside the Music Directory", str(newpath)) return False # Check if path if path is plausible oldcontenttype = self.EstimateContentTypeByPath(oldpath) newcontenttype = self.EstimateContentTypeByPath(newpath) if oldcontenttype != newcontenttype: logging.warning("Old path (\"%s\") was estimated as \"%s\", the new one (\"%s\") as \"%s\". \033[1;30m(Old path will not be renamed)", oldpath, oldcontenttype, newpath, newcontenttype) return False if oldcontenttype != "album": logging.warning("Old path (\"%s\") was estimated as \"%s\". An Album was expected. \033[1;30m(Old path will not be renamed)", oldpath, oldcontenttype) return False # Rename path logging.info("Renaming \033[0;36m%s\033[1;34m ➜ \033[0;36m%s", oldpath, newpath) try: success = self.Rename(oldpath, newpath) except Exception as e: logging.error("Renaming \"%s\" to \"%s\" failed with exception: %s \033[1;30m(Nothing changed, old path is still valid)", oldpath, newpath, str(e)) success = False if not success: logging.warning("Renaming \"%s\" to \"%s\" failed. \033[1;30m(Nothing changed, old path is still valid)", oldpath, newpath) return False 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`). The position of the artist should be plausible anyway. 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. 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, 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 oldpath = "Old Artist" newpath = "New Artist" musicdirectory.RenameArtistDirectory(oldpath, newpath) """ # Check if old path is a valid path to a file in the Music Directory if not self.IsDirectory(oldpath): logging.warning("Rename Artist Path failed because old path \"%s\" does not exists inside the Music Directory", str(oldpath)) return False # Check if new path addresses an already existing file if self.Exists(newpath): logging.warning("Rename Artist Path failed because new path \"%s\" does already exist inside the Music Directory", str(newpath)) return False # Check if path if path is plausible oldcontenttype = self.EstimateContentTypeByPath(oldpath) newcontenttype = self.EstimateContentTypeByPath(newpath) if oldcontenttype != newcontenttype: logging.warning("Old path (\"%s\") was estimated as \"%s\", the new one (\"%s\") as \"%s\". \033[1;30m(Old path will not be renamed)", oldpath, oldcontenttype, newpath, newcontenttype) return False if oldcontenttype != "artist": logging.warning("Old path (\"%s\") was estimated as \"%s\". An Artist was expected. \033[1;30m(Old path will not be renamed)", oldpath, oldcontenttype) return False # Rename path logging.info("Renaming \033[0;36m%s\033[1;34m ➜ \033[0;36m%s", oldpath, newpath) try: success = self.Rename(oldpath, newpath) except Exception as e: logging.error("Renaming \"%s\" to \"%s\" failed with exception: %s \033[1;30m(Nothing changed, old path is still valid)", oldpath, newpath, str(e)) success = False if not success: logging.warning("Renaming \"%s\" to \"%s\" failed. \033[1;30m(Nothing changed, old path is still valid)", oldpath, newpath) return False return True
[docs] def FixAttributes(self, path: Union[str, Path]): """ This method changes the access permissions and ownership of a file or directory. Only the addressed files or directory's permissions gets changed, not their parents. * File permissions: ``rw-rw-r--`` * Directory permissions: ``rwxrwxr-x`` To update the access permissions the method :meth:`musicdb.lib.filesystem.Filesystem.SetAttributes` is used. Args: path (str/Path): Path to an artist, album or song, relative to the music directory Returns: ``True`` on success, otherwise ``False`` Raises: ValueError: if path is neither a file nor a directory. """ if self.IsDirectory(path): permissions = "rwxrwxr-x" elif self.IsFile(path): permissions = "rw-rw-r--" else: raise ValueError("Path \""+str(path)+"\" is not a directory or file") success = self.SetAccessPermissions(path, permissions) return success
[docs] def AnalyseAlbumDirectoryName(self, albumdirname: str) -> dict: """ This method analyses the name of an album directory. If it does not follow the scheme of an album directory name, ``None`` gets returned, otherwise the albumname and release year. The scheme is the following: ``{releaseyear} - {albumname}``. The return value is a dictionay with the following keys: release: An integer with the release year name: A string with the album name Args: albumdirname (str): Directory name of an album without any ``"/"``. Returns: A dictionary with release year and the album name, or ``None`` Example: .. code-block:: python infos = fs.AnalyseAlbumDirectoryName("2000 - Testalbum") if infos: print(infos["name"]) # 2000 print(infos["release"]) # Testalbum """ album = {} try: album["release"] = int(albumdirname[0:4]) if albumdirname[4:7] != " - ": return None album["name"] = albumdirname[7:] except: return None return album
[docs] def AnalyseVideoFileName(self, videofilename: str) -> dict: """ This method analyses the name of a video file. Only the file name is expected, not a whole path! If it does not follow the scheme, ``None`` gets returned, otherwise all information encoded in the name as dictionary. The scheme is the following: ``{release} - {videoname}.{extension}`` The return value is a dictionary with the following keys: release: Release year as integer name: A string with the video name extension: file extension as string Args: videofilename (str): File name of an video without any ``"/"``. Returns: A dictionary on success, otherwise ``None`` Example: .. code-block:: python infos = fs.AnalyseVideoFileName("2000 - This is a Video.m4v") if infos: print(infos["release"]) # 2000 print(infos["name"]) # "This is a Video" print(infos["extension"]) # "m4v" """ if type(videofilename) == str: filename = videofilename else: filename = str(videofilename) video = {} # Check for " - " spacer try: if filename[4:7] != " - ": return None except: return None # Try to get the release year try: release = filename.split(" - ")[0] release = int(release) except: return None video["release"] = release # Extract name and extension try: filename = filename.split(" - ")[1] # remove release part video["name"] = self.GetFileName(filename) video["extension"] = self.GetFileExtension(filename) except: return None # Check extension if not video["extension"] in ["m4v", "mp4", "webm"]: return None return video
[docs] def AnalyseSongFileName(self, songfilename: str) -> dict: """ This method analyses the name of a song file. If it does not follow the scheme, ``None`` gets returned, otherwise all information encoded in the name as dictionary. The scheme is the following: ``{songnumber} {songname}.{extension}`` or ``{cdnumber}-{songnumber} {songname}.{extension}`` The return value is a dictionary with the following keys: cdnumber: CD number as integer or ``1`` if not given in the name number: Number of the song as integer name: A string with the song name extension: file extension as string Args: songfilename (str): File name of an song without any ``"/"``. Returns: A dictionary on success, otherwise ``None`` Example: .. code-block:: python infos = fs.AnalyseSongFileName("05 This is a Song.mp3") if infos: print(infos["cdnumber"]) # 1 print(infos["number"]) # 5 print(infos["name"]) # This is a Song print(infos["extension"]) # mp3 """ song = {} # Split number and name name = " ".join(songfilename.split(" ")[1:]) number = songfilename.split(" ")[0] # Split name and file extension song["name"] = self.GetFileName(name) song["extension"] = self.GetFileExtension(name) if not song["extension"] in [".flac", ".mp3", ".m4a", ".aac", ".MP3"]: return None # Split CD number and song number try: if "-" in number: [cdnumber, songnumber] = number.split("-") song["cdnumber"] = int(cdnumber) song["number"] = int(songnumber) else: song["cdnumber"] = 1 song["number"] = int(number) except: return None return song
[docs] def AnalysePath(self, musicpath): """ This method analyses a path to an artist, and album, a song or video and extracts all the information encoded in the path. For songs, path must consist of three parts: The artist directory, the album directory and the song file. For alums and videos only two parts are expected: The artist directory and the video file. For artist only one part. A valid path has one the following structures: * ``{artistname}/{albumrelease} - {albumname}/{songnumber} {songname}.{extension}`` * ``{artistname}/{albumrelease} - {albumname}/{cdnumber}-{songnumber} {songname}.{extension}`` * ``{artistname}/{videorelease} - {videoname}.{extension}`` * ``{artistname}/{albumrelease} - {albumname}`` * ``{artistname}`` The returned dictionary holds all the extracted information from the scheme listed above. The following entries exists but may be ``None`` depending if the path addresses a video or song. * artist * release * album * song * video * songnumber * cdnumber * extension In case there is no *cdnumber* specified for a song, this entry is ``1``. The names can have all printable Unicode characters and of cause spaces. If an error occurs because the path does not follow the scheme, ``None`` gets returned. This method also checks if the file or directory exists! The path must also address a file or directory inside the music directory. Anyway the path can be relative or absolute. Args: musicpath (str/Path): A path of a song including artist and album directory or a video including the artists directory. Returns: On success, a dictionary with information about the artist, album and song or video is returned. Otherwise ``None`` gets returned. """ if self.Exists(musicpath): logging.debug("Path \"%s\" does not exist.", str(musicpath)); path = self.TryRemoveRoot(musicpath) path = self.ToString(path) # Define all possibly used variables to a avoid undefined behavior result = {} result["artist"] = None result["album"] = None result["song"] = None result["video"] = None result["release"] = None result["songnumber"]= None result["cdnumber"] = None result["extension"] = None artist = None album = None song = None video = None # separate parts of the path parts = path.count("/") if parts == 0: # This must be an artist if self.IsDirectory(path): artist = path elif parts == 1: # This my be a video or an album (let's see if it is a directory) if self.IsDirectory(path): [artist, album] = path.split("/")[-2:] else: [artist, video] = path.split("/")[-2:] elif parts == 2: # This may be a song if self.IsFile(path): [artist, album, song] = path.split("/")[-3:] # analyze the artist information if artist: result["artist"] = artist else: logging.warning("Analysing \"%s\" failed!", path) logging.warning("Path cannot be split into three parts {artist}/{album}/{song} or two parts {artist}/{video}") return None # analyze the album information if album: albuminfos = self.AnalyseAlbumDirectoryName(album) if albuminfos == None: logging.warning("Analysing \"%s\" failed!", path) logging.warning("Unexpected album directory name. Expecting \"{year} - {name}\"") return None result["release"] = albuminfos["release"] result["album"] = albuminfos["name"] # analyze the song information if song: try: songname = song.split(" ")[1:] songnumber = song.split(" ")[0] try: [cdnumber, songnumber] = songnumber.split("-") except: cdnumber = 1 songnumber = int(songnumber) cdnumber = int(cdnumber) songname = " ".join(songname) extension = os.path.splitext(songname)[1][1:] # get extension without leading "." songname = os.path.splitext(songname)[0] # remove extension except: logging.warning("Analysing \"%s\" failed!", path) logging.warning("Unexpected song file name. Expected \"[{cdnumber}-]{songnumber} {songname}.{ending}\".") return None result["song"] = songname result["songnumber"] = songnumber result["cdnumber"] = cdnumber result["extension"] = extension # analyze the video information if video: videoinfos = self.AnalyseVideoFileName(video) if videoinfos == None: logging.warning("Analyzing \"%s\" failed!", path) logging.warning("Unexpected video file name. Expecting \"{year} - {name}.{extension}\"") return None result["release"] = videoinfos["release"] result["video"] = videoinfos["name"] result["extension"] = videoinfos["extension"] return result
[docs] def TryAnalysePathFor(self, target="all", path=None): """ This method checks if a path is valid for a specific target. The check is done in the following steps: #. Get all song paths #. Apply an information extraction on all found song paths using :meth:`~AnalysePath` This guarantees, that all files are valid to process with MusicDB. Args: target (str): Optional, default value is ``"all"``. One of the following targets: ``"all"``, ``"artist"``, ``"album"`` or ``"song"`` path (str): Optional, default value is ``None``. Path to an artist, album or song. If target is ``"all"``, path can be ``None``. Returns: ``True`` If the path is valid for the given target. Otherwise ``False`` gets returned. Raises: ValueError: when *target* is not ``"all"``, ``"artist"``, ``"album"`` or ``"song"`` """ if path == None and target != "all": logging.error("Path to check if it is a valid for %s may not be None!", target) return False # Get all song paths try: if target == "all": artistpaths = self.GetSubdirectories(path, self.ignoreartists) albumpaths = self.GetSubdirectories(artistpaths, self.ignorealbums) songpaths = self.GetFiles(albumpaths, self.ignoresongs) elif target == "artist": albumpaths = self.GetSubdirectories(path, self.ignorealbums) songpaths = self.GetFiles(albumpaths, self.ignoresongs) elif target == "album": songpaths = self.GetFiles(path, self.ignoresongs) elif target == "song": songpaths = [path] else: raise ValueError("target not in {all, artist, album, song}") except Exception as e: logging.error("The given path (\"%s\") was not a valid %s-path!\033[1;30m (%s)", path, target, str(e)) return False n = len(songpaths) if n < 1: logging.error("No songs in %s-path (%s)", target, path) return False for songpath in songpaths: if not self.Exists(songpath): logging.error("The song path %s does not exist.", songpath) return False # Scan all song pathes - if they are not analysable, give an error for songpath in songpaths: result = self.AnalysePath(songpath) if result == False: logging.error("Invalid path: " + songpath) return False return True
[docs] def EstimateContentTypeByPath(self, path): """ This method tries to figure out if the path addresses an Artist, Album, Song or Video by analyzing the path. If is *not* checked if the path fulfills the Music Naming Scheme (See: :doc:`/usage/music`). The path must be relative. It is not checked if the file or directory exists. The result is just a guess an must be checked in detail for further processing. For example with :meth:`TryAnalysePathFor`. Args: path (str): Possible relative path for an Artist, Album, Song or Video Returns: A string ``"artist"``, ``"album"``, ``"song"``, ``"video"`` or ``None`` if none can be guessed. """ contenttype = None parts = path.count("/") + 1 # n slash means n+1 parts of a path suffix = self.GetFileExtension(path) # If there is a file extension, it may be a file logging.debug("Estimating content type: parts: %i, suffix: %s", parts, suffix) # Try to check if path addresses a file. # This only works when the path exists. # If path does not exists, use the suffix-knowledge and just guess. isfile = False if self.Exists(path): isfile = self.IsFile(path) elif suffix != None: # Now check if something points against a file if suffix in ["m4a", "flac", "aac", "mp4", "mp3", "m4v"]: isfile = True else: isfile = False # Estimate path type if parts == 1: contenttype = "artist" elif parts == 2: if isfile: contenttype = "video" else: contenttype = "album" elif parts == 3: if isfile: contenttype = "song" return contenttype
[docs] def EvaluateAlbumDirectory(self, albumpath): """ This method checks if a directory path is a valid album directory. Despite :meth:`~TryAnalysePath` and :meth:`AnalyseAlbumDirectoryName` this method does not care about the naming scheme. It checks the actual content and directory inside the file system. A valid album directory must fulfill the following criteria: * It must be an existing directory * MusicDB must have read and write access to this directory * The directory must only contain files, no sub directories * All files inside the directory must be readable and writable * At least one file inside the directory must be a song file Args: albumpath (str): Path to the album to check Returns: ``True if the directory is a valid album directory. Otherwise ``False``. """ if not self.IsDirectory(albumpath): logging.debug("Invalid album directory: %s does not address an existing directory.", str(albumpath)); return False if not self.ap.IsWritable(albumpath): logging.debug("Invalid album directory: MusicDB has no write access to %s.", albumpath); return False albumfiles = self.ListDirectory(albumpath) songfound = False for albumfile in albumfiles: if not self.IsFile(albumfile): logging.debug("Invalid album directory: Album directory %s has at least one sub directory (%s) which should not be the case.", albumpath, albumfile); return False fileextension = self.GetFileExtension(albumfile) if fileextension in ["m4a", "flac", "aac", "mp4", "mp3"]: songfound = True if not self.ap.IsWritable(albumfile): logging.debug("Invalid album directory: Album directory %s has at least one read-only file (%s) which should not be the case.", albumpath, albumfile); return False if not songfound: logging.debug("Invalid album directory: Album directory %s has no song files.", albumpath); return False return True
[docs] def EvaluateArtistDirectory(self, artistpath): """ This method checks if a directory is a valid artist directory. In contrast to :meth:`~TryAnalysePath` this method does not care about the naming scheme. It checks the actual content, directory and sub directories inside the file system. A valid artist directory must fulfill the following criteria: * It must be an existing directory * MusicDB must have read and write access to this directory * All sub directories and files must be writable * There must be at least one valid album directory (Checked via :meth:`~EvaluateAlbumDirectory` or a video file Args: artistpath (str): Path to the artist to check Returns: ``True if the directory is a valid artist directory. Otherwise ``False``. """ if not self.IsDirectory(artistpath): logging.debug("Invalid artist directory: %s does not address an existing directory.", str(artistpath)); return False if not self.ap.IsWritable(artistpath): logging.debug("Invalid artist directory: MusicDB has no write access to %s.", artistpath); return False artistcontent = self.ListDirectory(artistpath) videofound = False albumfound = False for path in artistcontent: if not self.ap.IsWritable(path): logging.debug("Invalid artist directory: Artist directory %s has at least one read-only file or sub-directory (%s) which should not be the case.", artistpath, path); return False if self.IsFile(path): fileextension = self.GetFileExtension(path) if fileextension in ["webm", "mp4", "m4v"]: videofound = True else: if self.EvaluateAlbumDirectory(path): albumfound = True if not videofound and not albumfound: logging.debug("Invalid artist directory: Artist directory %s has no albums and no video files.", artistpath); return False return True
[docs] def EvaluateMusicFile(self, musicpath): """ This method checks if a file is a valid music file. In contrast to :meth:`~TryAnalysePath` this method does not care about the naming scheme. It checks the actual content inside the file system. A valid music file must fulfill the following criteria: * It must be an existing file * MusicDB must have read and write access to this file Args: musicpath (str): Path to the music file to check Returns: ``True if the file is a valid music file. Otherwise ``False``. """ if not self.IsFile(musicpath): logging.debug("Invalid music file: %s does not address an existing file.", str(musicpath)); return False if not self.ap.IsWritable(musicpath): logging.debug("Invalid music file: MusicDB has no write access to %s.", musicpath); return False return True
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4