Пример #1
0
class SongView(ListView, ButtonView):
    def __init__(self, config, albumpath, title, x, y, w, h):
        ListView.__init__(self, title, x, y, w, h)
        ButtonView.__init__(self, align="center")
        self.AddButton("↑", "Go up")
        self.AddButton("↓", "Go down")
        self.AddButton("c", "Clean name")
        self.AddButton("e", "Edit name")
        #self.AddButton("␣", "Toggle")
        #self.AddButton("↵", "Commit")
        #self.AddButton("␛", "Cancel")

        # elements are a tuple (original path, new path)

        self.cfg = config
        self.fs = Filesystem(self.cfg.music.path)
        self.albumpath = albumpath

        self.nameinput = FileNameInput()
        self.numberinput = TextInput()
        self.cdnuminput = TextInput()
        dialogh = 2 + 3
        self.dialog = Dialog("Rename Song", self.x, self.y + 1, self.w,
                             dialogh)
        self.dialog.AddInput("Song name:", self.nameinput, "Correct name only")
        self.dialog.AddInput("Song number:", self.numberinput,
                             "Song number only")
        self.dialog.AddInput("CD number:", self.cdnuminput,
                             "CD number or nothing")
        self.dialogmode = False

    def FindSongs(self):
        files = self.fs.GetFiles(self.albumpath, self.cfg.music.ignoresongs)
        songs = []
        # Only take audio files into account - ignore images and booklets
        for f in files:
            extension = self.fs.GetFileExtension(f)
            if extension in ["mp3", "flac", "m4a", "aac"]:
                songs.append((f, f))
        return songs

    def CleanFileNames(self):
        for index, element in enumerate(self.elements):
            origpath = element[0]
            path = element[1]
            directory = self.fs.GetDirectory(path)
            filename = self.fs.GetFileName(path)
            extension = self.fs.GetFileExtension(path)
            seg = self.FileNameSegments(filename)

            newfilename = filename[seg["number"]:seg["gap"]]
            newfilename += filename[seg["name"]:]
            newfilename = unicodedata.normalize("NFC", newfilename)

            newpath = os.path.join(directory, newfilename + "." + extension)
            self.elements[index] = (origpath, newpath)

    # no path, no file extension!
    # returns indices of name segments
    def FileNameSegments(self, filename):
        seg = {}

        # Start of song number
        m = re.search("\d", filename)
        if m:
            seg["number"] = m.start()
        else:
            seg["number"] = 0

        # End of song number (1 space is necessary)
        m = re.search("\s", filename[seg["number"]:])
        if m:
            seg["gap"] = seg["number"] + 1 + m.start()
        else:
            seg["gap"] = seg["number"] + 1

        # Find start of song name
        m = re.search("\w", filename[seg["gap"]:])
        if m:
            seg["name"] = seg["gap"] + m.start()
        else:
            seg["name"] = seg["gap"]

        return seg

    def UpdateUI(self):
        newsongs = self.FindSongs()
        self.SetData(newsongs)

    def onDrawElement(self, element, number, maxwidth):
        oldpath = element[0]
        path = element[1]
        width = maxwidth
        filename = self.fs.GetFileName(path)
        extension = self.fs.GetFileExtension(path)
        analresult = self.fs.AnalyseSongFileName(filename + "." + extension)

        # Render validation
        if not analresult:
            validation = "\033[1;31m ✘ "
        else:
            validation = "\033[1;32m ✔ "
        width -= 3

        # Render file name
        renderedname = ""
        width -= len(filename)
        seg = self.FileNameSegments(filename)
        renderedname += "\033[1;31m\033[4m" + filename[
            0:seg["number"]] + "\033[24m"
        renderedname += "\033[1;34m" + filename[seg["number"]:seg["gap"]]
        renderedname += "\033[1;31m\033[4m" + filename[
            seg["gap"]:seg["name"]] + "\033[24m"
        renderedname += "\033[1;34m" + filename[seg["name"]:]

        # Render file extension
        fileextension = "." + extension
        fileextension = fileextension[:width]
        fileextension = fileextension.ljust(width)
        return validation + "\033[1;34m" + renderedname + "\033[1;30m" + fileextension

    def Draw(self):
        if self.dialogmode == True:
            pass
        else:
            ListView.Draw(self)
            x = self.x + 1
            y = self.y + self.h - 1
            w = self.w - 2
            ButtonView.Draw(self, x, y, w)

    def HandleKey(self, key):
        if self.dialogmode == True:
            if key == "enter":  # Commit dialog inputs
                songname = self.nameinput.GetData()
                songnumber = self.numberinput.GetData()
                cdnumber = self.cdnuminput.GetData()

                element = self.dialog.oldelement
                path = element[
                    1]  # the editable path is 1, 0 is the original path
                directory = self.fs.GetDirectory(path)
                extension = self.fs.GetFileExtension(path)

                if len(songnumber) == 1:
                    songnumber = "0" + songnumber
                if cdnumber:
                    songnumber = cdnumber + "-" + songnumber

                newpath = os.path.join(
                    directory, songnumber + " " + songname + "." + extension)
                self.SetSelectedData((element[0], newpath))

                self.dialogmode = False
                self.Draw()  # show list view instead of dialog

            elif key == "escape":
                self.dialogmode = False
                self.dialog.oldname = None  # prevent errors by leaving a clean state
                self.Draw()  # show list view instead of dialog
                # reject changes

            else:
                self.dialog.HandleKey(key)

        else:
            if key == "up" or key == "down":
                ListView.HandleKey(self, key)

            elif key == "c":
                self.CleanFileNames()

            elif key == "e":  # edit name
                element = self.GetSelectedData()
                editpath = element[1]
                filename = self.fs.GetFileName(editpath)
                seg = self.FileNameSegments(filename)
                songnumber = filename[seg["number"]:seg["gap"]].strip()
                songname = filename[seg["name"]:].strip()

                if "-" in songnumber:
                    cdnumber = songnumber.split("-")[0].strip()
                    songnumber = songnumber.split("-")[1].strip()
                else:
                    cdnumber = ""

                self.nameinput.SetData(songname)
                self.numberinput.SetData(songnumber)
                self.cdnuminput.SetData(cdnumber)

                self.dialog.oldelement = element
                self.dialog.Draw()
                self.dialogmode = True
Пример #2
0
class Lycra(object):
    """
    This class does the main lyrics management.

    Args:
        config: MusicDB Configuration object.

    Raises:
        TypeError: when *config* is not of type :class:`~lib.cfg.musicdb.MusicDBConfig`
    """
    def __init__(self, config):

        if type(config) != MusicDBConfig:
            logging.error("Config-class of unknown type!")
            raise TypeError("config argument not of type MusicDBConfig")

        logging.debug("Crawler path is %s", CRAWLERPATH)

        self.config = config
        self.lycradb = LycraDatabase(self.config.lycra.dbpath)
        self.fs = Filesystem(CRAWLERPATH)
        self.crawlers = None

    def LoadCrawlers(self):
        """
        This method loads all crawlers inside the crawler directory.

        .. warning::

            Changes at crawler may not be recognized until the whole application gets restarted.
            Only new added crawler gets loaded.
            Already loaded crawler are stuck at Pythons module cache.

        Returns:
            ``None``
        """
        # Get a list of all modules
        crawlerfiles = self.fs.GetFiles(".")
        modulenames = [
            self.fs.GetFileName(x) for x in crawlerfiles
            if self.fs.GetFileExtension(x) == "py"
        ]
        if len(modulenames) == 0:
            logging.warning(
                "No modules found in \"%s\"! \033[1;30m(… but crawler cache is still usable.)",
                self.fs.AbsolutePath(CRAWLERPATH))
            self.crawlers = None
            return None

        # load all modules
        self.crawlers = []
        for modulename in modulenames:
            modfp, modpath, moddesc = imp.find_module(modulename,
                                                      [CRAWLERPATH])

            try:
                logging.debug("Loading %s …", str(modpath))
                module = imp.load_module(modulename, modfp, modpath, moddesc)
            except Exception as e:
                logging.error(
                    "Loading Crawler %s failed with error: %s! \033[1;30m(Ignoring this specific Crawler)",
                    str(e), str(modpath))
            finally:
                # Since we may exit via an exception, close fp explicitly.
                if modfp:
                    modfp.close()

            crawler = {}
            crawler["module"] = module
            crawler["modulename"] = modulename
            self.crawlers.append(crawler)

        if len(self.crawlers) == 0:
            logging.warning(
                "No crawler loaded from \"%s\"! \033[1;30m(… but crawler cache is still usable.)",
                self.fs.AbsolutePath(CRAWLERPATH))
            self.crawlers = None
        return None

    def RunCrawler(self, crawler, artistname, albumname, songname, songid):
        """
        This method runs a specific crawler.
        This crawler gets all information available to search for a specific songs lyric.

        This method is for class internal use.
        When using this class, call :meth:`~mdbapi.lycra.Lycra.CrawlForLyrics` instead of calling this method directly.
        Before calling this method, :meth:`~mdbapi.lycra.Lycra.LoadCrawlers` must be called.

        The crawler base class :class:`lib.crawlerapi.LycraCrawler` catches all exceptions so that they do not net to be executed in an try-except environment.

        Args:
            crawler (str): Name of the crawler. If it addresses the file ``lib/crawler/example.py`` the name is ``example``
            artistname (str): The name of the artist as stored in the MusicDatabase
            albumname (str): The name of the album as stored in the MusicDatabase
            songname (str): The name of the song as stored in the MusicDatabase
            songid (int): The ID of the song to associate the lyrics with the song

        Returns:
            ``None``
        """
        crawlerclass = getattr(crawler["module"], crawler["modulename"])
        crawlerentity = crawlerclass(self.lycradb)
        crawlerentity.Crawl(artistname, albumname, songname, songid)
        return None

    def CrawlForLyrics(self, artistname, albumname, songname, songid):
        """
        Loads all crawler from the crawler directory via :meth:`~mdbapi.lycra.Lycra.LoadCrawlers` 
        and runs them via :meth:`~mdbapi.lycra.Lycra.RunCrawler`.

        Args:
            artistname (str): The name of the artist as stored in the music database
            albumname (str): The name of the album as stored in the music database
            songname (str): The name of the song as stored in the music database
            songid (int): The ID of the song to associate the lyrics with the song

        Returns:
            ``False`` if something went wrong. Otherwise ``True``. (This is *no* indication that there were lyrics found!)
        """
        # Load / Reload crawlers
        try:
            self.LoadCrawlers()
        except Exception as e:
            logging.error(
                "Loading Crawlers failed with error \"%s\"! \033[1;30m(… but crawler cache is still usable.)",
                str(e))
            return False

        if not self.crawlers:
            return False

        for crawler in self.crawlers:
            self.RunCrawler(crawler, artistname, albumname, songname, songid)

        return True

    def GetLyrics(self, songid):
        """
        This method returns the lyrics of a song.
        See :meth:`lib.db.lycradb.LycraDatabase.GetLyricsFromCache`
        """
        return self.lycradb.GetLyricsFromCache(songid)
Пример #3
0
class MusicCache(object):
    """
    Args:
        config: MusicDB configuration object
        database: MusicDB database

    Raises:
        TypeError: when *config* or *database* not of type :class:`~lib.cfg.musicdb.MusicDBConfig` or :class:`~lib.db.musicdb.MusicDatabase`
    """
    def __init__(self, config, database):
        if type(config) != MusicDBConfig:
            print(
                "\033[1;31mFATAL ERROR: Config-class of unknown type!\033[0m")
            raise TypeError("config argument not of type MusicDBConfig")
        if type(database) != MusicDatabase:
            print(
                "\033[1;31mFATAL ERROR: Database-class of unknown type!\033[0m"
            )
            raise TypeError("database argument not of type MusicDatabase")

        self.db = database
        self.cfg = config
        self.fs = Filesystem(self.cfg.music.cache)
        self.fileprocessor = Fileprocessing(self.cfg.music.cache)
        self.artworkcache = ArtworkCache(self.cfg.artwork.path)

    def GetAllCachedFiles(self):
        """
        This method returns three lists of paths of all files inside the cache.
        The tree lists are the following:

            #. All artist directories
            #. All album paths
            #. All song paths

        Returns:
            A tuple of three lists: (Artist-Paths, Album-Paths, Song-Paths)

        Example:

            .. code-block:: python

                (artists, albums, songs) = cache.GetAllCachedFiles()

                print("Albums in cache:")
                for path in albums:
                    name = musicdb.GetAlbumByPath(path)["name"]
                    print(" * " + name)

                print("Files in cache:")
                for path in songs:
                    print(" * " + path)
        """
        # Get all files from the cache
        artistpaths = self.fs.ListDirectory()
        albumpaths = self.fs.GetSubdirectories(artistpaths)
        songpaths = self.fs.GetFiles(albumpaths)

        return artistpaths, albumpaths, songpaths

    def RemoveOldArtists(self, cartistpaths, mdbartists):
        """
        This method removes all cached artists when they are not included in the artist list from the database.

        ``cartistpaths`` must be a list of artist directories with the artist ID as directory name.
        From these paths, a list of available artist ids is made and compared to the artist ids from the list of artists returned by the database (stored in ``mdbartists``)

        Is there a path/ID in ``cartistpaths`` that is not in the ``mdbartists`` list, the directory gets removed.
        The pseudo code can look like this:

            .. code-block:: python

                for path in cartistpaths:
                    if int(path) not in [mdbartists["id"]]:
                        self.fs.RemoveDirectory(path)

        Args:
            cartistpaths (list): a list of artist directories in the cache
            mdbartists (list): A list of artist rows from the Music Database

        Returns:
            *Nothing*
        """
        artistids = [artist["id"] for artist in mdbartists]
        cachedids = [int(path) for path in cartistpaths]

        for cachedid in cachedids:
            if cachedid not in artistids:
                self.fs.RemoveDirectory(str(cachedid))

    def RemoveOldAlbums(self, calbumpaths, mdbalbums):
        """
        This method compares the available album paths from the cache with the entries from the Music Database.
        If there are albums that do not match the database entries, then the cached album will be removed.

        Args:
            calbumpaths (list): a list of album directories in the cache (scheme: "ArtistID/AlbumID")
            mdbalbums (list): A list of album rows from the Music Database

        Returns:
            *Nothing*
        """
        # create "artistid/albumid" paths
        validpaths = [
            os.path.join(str(album["artistid"]), str(album["id"]))
            for album in mdbalbums
        ]

        for cachedpath in calbumpaths:
            if cachedpath not in validpaths:
                self.fs.RemoveDirectory(cachedpath)

    def RemoveOldSongs(self, csongpaths, mdbsongs):
        """
        This method compares the available song paths from the cache with the entries from the Music Database.
        If there are songs that do not match the database entries, then the cached song will be removed.

        Args:
            csongpaths (list): a list of song files in the cache (scheme: "ArtistID/AlbumID/SongID:Checksum.mp3")
            mdbsongs (list): A list of song rows from the Music Database

        Returns:
            *Nothing*
        """
        # create song paths
        validpaths = []
        for song in mdbsongs:
            path = self.GetSongPath(song)
            if path:
                validpaths.append(path)

        for cachedpath in csongpaths:
            if cachedpath not in validpaths:
                self.fs.RemoveFile(cachedpath)

    def GetSongPath(self, mdbsong, absolute=False):
        """
        This method returns a path following the naming scheme for cached songs (``ArtistID/AlbumID/SongID:Checksum.mp3``).
        It is not guaranteed that the file actually exists.

        Args:
            mdbsong: Dictionary representing a song entry form the Music Database
            absolute: Optional argument that can be set to ``True`` to get an absolute path, not relative to the cache directory.

        Returns:
            A (possible) path to the cached song (relative to the cache directory, ``absolute`` got not set to ``True``).
            ``None`` when there is no checksum available. The checksum is part of the file name.
        """
        # It can happen, that there is no checksum for a song.
        # For example when an old installation of MusicDB got not updated properly.
        # Better check if the checksum is valid to avoid any further problems.
        if len(mdbsong["checksum"]) != 64:
            logging.error(
                "Invalid checksum of song \"%s\": %s \033[1;30m(64 hexadecimal digit SHA265 checksum expected. Try \033[1;34mmusicdb repair --checksums \033[1;30mto fix the problem.)",
                mdbsong["path"], mdbsong["checksum"])
            return None

        path = os.path.join(str(mdbsong["artistid"]),
                            str(mdbsong["albumid"]))  # ArtistID/AlbumID
        path = os.path.join(path,
                            str(mdbsong["id"]))  # ArtistID/AlbumID/SongID
        path += ":" + mdbsong[
            "checksum"] + ".mp3"  # ArtistID/AlbumID/SongID:Checksum.mp3

        if absolute:
            path = self.fs.AbsolutePath(path)

        return path

    def Add(self, mdbsong):
        """
        This method checks if the song exists in the cache.
        When it doesn't, the file will be created (this may take some time!!).

        This process is done in the following steps:

            #. Check if song already cached. If it does, the method returns with ``True``
            #. Create directory tree if it does not exist. (``ArtistID/AlbumID/``)
            #. Convert song to mp3 (320kbp/s) and write it into the cache.
            #. Update ID3 tags. (ID3v2.3.0, 500x500 pixel artworks)

        Args:
            mdbsong: Dictionary representing a song entry form the Music Database

        Returns:
            ``True`` on success, otherwise ``False``
        """
        path = self.GetSongPath(mdbsong)
        if not path:
            return False

        # check if file exists, and create it when not.
        if self.fs.IsFile(path):
            return True

        # Create directory if not exists
        directory = os.path.join(str(mdbsong["artistid"]),
                                 str(mdbsong["albumid"]))  # ArtistID/AlbumID
        if not self.fs.IsDirectory(directory):
            self.fs.CreateSubdirectory(directory)

        # Create new mp3
        srcpath = os.path.join(self.cfg.music.path, mdbsong["path"])
        dstpath = self.fs.AbsolutePath(path)
        retval = self.fileprocessor.ConvertToMP3(srcpath, dstpath)
        if retval == False:
            logging.error("Converting %s to %s failed!", srcpath, dstpath)
            return False
        os.sync()

        # Optimize new mp3
        mdbalbum = self.db.GetAlbumById(mdbsong["albumid"])
        mdbartist = self.db.GetArtistById(mdbsong["artistid"])

        try:
            relartworkpath = self.artworkcache.GetArtwork(
                mdbalbum["artworkpath"], "500x500")
        except Exception as e:
            logging.error(
                "Getting artwork from cache failed with exception: %s!",
                str(e))
            logging.error("   Artwork: %s, Scale: %s", mdbalbum["artworkpath"],
                          "500x500")
            return False

        absartworkpath = os.path.join(self.cfg.artwork.path, relartworkpath)

        retval = self.fileprocessor.OptimizeMP3Tags(
            mdbsong,
            mdbalbum,
            mdbartist,
            srcpath=path,
            dstpath=path,
            absartworkpath=absartworkpath,
            forceID3v230=True)
        if retval == False:
            logging.error("Optimizing %s failed!", path)
            return False

        return True