#!/bin/python
import curses
import os
import posixpath
import requests
from mpd import MPDClient
import ffmpeg
import pixcat
import time
import configparser
import ueberzug.lib.v0 as ueberzug
from PIL import Image, ImageDraw
from colorthief import ColorThief
import math


# Get config
config = configparser.ConfigParser()
config.read(os.path.expanduser("~/.config/miniplayer/config"))

if "player" not in config.sections():
    config["player"] = {"font_width":      11,
                        "font_height":     24,
                        "album_art_only":  False,
                        "volume_step":     5,
                        "auto_close":      False,
                        "show_playlist":   True,
                        }

if "art" not in config.sections():
    config["art"] = {"music_directory":  "~/Music",
                     "image_method":     "pixcat",
                    }

if "mpd" not in config.sections():
    config["mpd"] = {"host": "localhost",
                     "port": "6600",
                    }

if "theme" not in config.sections():
    config["theme"] = {"accent_color": "yellow",
                       "bar_color":    "green",
                       "time_color":   "yellow",
                       "bar_body":     "─",
                       "bar_head":     "╶"
                      }

# Initialise keybindings
default_bindings = {">":     "next_track",
                    "<":     "last_track",
                    "+":     "volume_up",
                    "-":     "volume_down",
                    "p":     "play_pause",
                    "q":     "quit",
                    "h":     "help",
                    "i":     "toggle_info",
                    "down":  "select_down",
                    "up":    "select_up",
                    "enter": "select"
                    }

if "keybindings" not in config.sections():
    config["keybindings"] = default_bindings

# Ensure compatibility with older configs
deprecated_field_notice = ("The config option `{field}` under the `player` "
                           "section is deprecated and will be removed in the "
                           "future. Use the config option `{field}` under the "
                           "`art` section instead.")

deprecated_fields_present = False
for field in ["music_directory", "image_method"]:
    if config.has_option("player", field):
        deprecated_fields_present = True
        print(deprecated_field_notice.format(field=field))
        
        if not config.has_option("art", field):
            config["art"][field] = config["player"][field]

if deprecated_fields_present:
    input("(Press <Enter> to continue) ...")

# Load configured keybindings
keybindings = config["keybindings"]

# Unbound actions get initialised with their default keys
# except if the keys are being used for something else
for key, action in default_bindings.items():
    if (
            action not in keybindings.values()
            and key not in keybindings.keys()
    ):
        keybindings[key] = action

player_config = config["player"]
art_config    = config["art"]
mpd_config    = config["mpd"]
theme_config  = config["theme"]


# FPS
FPS = 20

# Image ratio
# Change this to match the (width, height) of your font.
IMAGERATIO = (player_config.getint("font_width", 11),
              player_config.getint("font_height", 24)
              )

# MPD config
MPDHOST = mpd_config.get("host", "localhost")
MPDPORT = mpd_config.getint("port", 6600)
MPDPASS = mpd_config.get("pass", False)

# What to use to draw images
IMAGEMETHOD = art_config.get("image_method", "pixcat")

# Volume step
VOLUMESTEP = player_config.getint("volume_step", 5)

# Autoclose boolean
AUTOCLOSE = player_config.getboolean("auto_close", False)

# Playlist padding
PLAYLISTMARGIN = 4

# Config option to display the playlist
DISABLEPLAYLIST = not player_config.getboolean("show_playlist", True)


def albumArtSize(album_space, window_width):
    """
    Calculates the album art size given the window width and the height
    of the album art space
    """
    if window_width * IMAGERATIO[0] > album_space * IMAGERATIO[1]:
        image_width_px = album_space * IMAGERATIO[1]
    else:
        image_width_px = window_width * IMAGERATIO[0]

    image_width  = int(image_width_px  // IMAGERATIO[0])
    image_height = int(image_width_px  // IMAGERATIO[1])

    return image_width_px, image_width, image_height


def colorNameToCurses(name):
    """
    A function that takes in the name of a color and returns
    the corresponding curses.COLOR_{name}
    """

    converter = {
        "black":    0,
        "red":      1,
        "green":    2,
        "yellow":   3,
        "blue":     4,
        "magenta":  5,
        "cyan":     6,
        "white":    -1,
        "auto":     "auto"
    }

    if name not in converter.keys():
        return None
    else:
        return converter[name]


class Player:
    def __init__(self):
        # Error variable
        self.error = None

        # Curses initialisation
        self.stdscr = curses.initscr()
        self.stdscr.nodelay(True)
        self.stdscr.keypad(True)

        # Curses config
        curses.noecho()
        curses.curs_set(0)

        curses.cbreak()

        curses.start_color()
        curses.use_default_colors()

        # Color initialisation
        self.AUTO_COLOR = curses.COLOR_WHITE

        
        # Figure out which pairs we need to auto color
        self.auto_pairs = []

        color_array = [
            theme_config.get("accent_color", "yellow"),
            theme_config.get("time_color",   "yellow"), 
            theme_config.get("bar_color",    "green" )
        ]

        i = 0
        while i < len(color_array):
            # convert color
            color = color_array[i]
            color = colorNameToCurses(color)

            # check for auto color
            if color == "auto":
                color = self.AUTO_COLOR
                self.auto_pairs.append(i+1)     

            # Verify that the color is ok
            if color is None:
                self.error = f"'{color_array[i]}' is not a valid color!"
                raise ValueError(self.error)

            color_array[i] = color

            i += 1

        # Init pairs
        curses.init_pair(1, color_array[0],     -1) # accent color
        curses.init_pair(2, color_array[1],     -1) # time color
        curses.init_pair(3, color_array[2],     -1) # bar color

        # Get progress bar characters
        self.bar_body = theme_config.get("bar_body", "─")
        self.bar_head = theme_config.get("bar_head", "╶")

        # Check if bar head and bar body are of proper length
        if len(self.bar_body) != 1:
            raise ValueError("Progress bar body must be 1 character long! Not '{bar_body}'")
    
        if len(self.bar_head) != 1:
            raise ValueError("Progress bar head must be 1 character long! Not '{bar_head}'")

        # MPD init
        self.client = MPDClient()
        self.client.connect(MPDHOST, MPDPORT)
        if MPDPASS:
            self.client.password(MPDPASS)

        self.last_song = None

        # Album art HTTP server
        
        if art_config.get("http_base_url"):
            self.art_http_session = requests.Session()

        # Album art only flag
        self.album_art_only = player_config.getboolean("album_art_only", False)

        # Screen size
        maxyx = self.stdscr.getmaxyx()
        self.screen_height, self.screen_width = maxyx

        # Album art window
        self.art_window_height, self.art_window_width = self.albumArtWinWidth(*maxyx)
        self.art_win = curses.newwin(
            self.art_window_height, self.art_window_width,
            0, 0
        )

        # Playlist window
        if self.playlistFits(*maxyx) and not self.album_art_only:
            self.draw_playlist = True
            self.playlist_window_width = maxyx[1] - self.art_window_width - PLAYLISTMARGIN
            self.playlist_window_height = maxyx[0]

            self.playlist_win = curses.newwin(
                self.playlist_window_height, self.playlist_window_width,
                0, self.art_window_width + PLAYLISTMARGIN
            )
        else:
            self.draw_playlist = False
            self.playlist_win = None

        self.text_start = int(self.art_window_height - 5)
        self.album_space = self.text_start - 2

        # Calculate the size of the image
        self.image_width_px, self.image_width, self.image_height = albumArtSize(self.album_space, self.art_window_width)
        self.image_y_pos = (self.album_space - self.image_height) // 2 + 1

        # Album art location
        self.album_art_loc = "/tmp/aartminip.png"

        # Toggle for help menu
        self.help = False
        self.cleared = False

        # Ueberzug placement
        self.art_placement = None

        # Update needed flag
        self.update_needed = False

        # Color update needed flag
        self.color_update_needed = bool(self.auto_pairs)

        # Flag to check if any music has been played
        self.has_music_been_played = False

        # A counter to check how long since playlist has moved
        self.control_cycle = 0

        # Selected song in playlist
        self.selected_song = 0


    def getDominantColor(self):
        """
        A function that finds the dominant color in the album art,
        finds the closest curses color to that and sets the foreground
        color.
        """

        COLOR_COUNT = 5

        def colorScore(r, g, b):
            """
            A function that takes in the RGB representation of a
            color and assigns it a score based on its perceived
            brightness and saturation.

            The score is calculated as follows:
                1. Convert the RGB values to HSV saturation
                2. Calculate brightness with
                        0.2126*r + 0.7152*g + 0.0722*b
                3. Check if either the brightness is below
                   a threshold or the color is not very
                   saturated. If so, assign it a score of
                        mean(saturation, brightness) / 100
                4. Otherwise, assign it a score of
                        sqrt(brightness^2 * saturation)
            """
            # Map RGB to [0,1]
            r /= 255
            g /= 255
            b /= 255

            maximum = max(r, g, b)
            minimum = min(r, g, b)

            # Calculate saturation
            saturation = 0
            if maximum > 0:
                saturation = (maximum - minimum) / (maximum)

            # Calculate brightness
            brightness = (0.2126*r + 0.7152*g + 0.0722*b)

            # Brightness and saturation thresholds
            if brightness < 0.35 or saturation < 0.1:
                return 0.01 * (brightness + saturation) / 2
            else:
                return math.sqrt(brightness**2 * saturation)

        def weight(x):
            """
            A weight function to transform the color scores based
            on their frequency
            """
            return 1/(x+1)**2
            

        # Init thief and get palette
        try:
            color_thief = ColorThief(self.album_art_loc)
            palette = color_thief.get_palette(color_count=COLOR_COUNT, quality=100)
        except (OSError, FileNotFoundError):
            self.color_update_needed = True
            return

        # Create weight list and assign weights
        weight_list = [weight(x/COLOR_COUNT) for x in range(COLOR_COUNT)]
        palette = list(zip(weight_list, palette))

        # Sort palette based on score
        palette = sorted(
            palette, 
            reverse=True, 
            key=lambda x: x[0]*colorScore(*x[1])
        )

        # Get the dominant color
        dominant_color = palette[0][1]

        # Cast the color to curses range
        col = [round(1000 * val/255) for val in dominant_color]

        # Set the color
        curses.init_color(self.AUTO_COLOR, *col)
        
        for pair in self.auto_pairs:
            curses.init_pair(pair, self.AUTO_COLOR, -1)

        self.color_update_needed = False


    def playlistFits(self, height, width):
        """
        A function that checks if the playlist display should be drawn
        based on the provided height and width
        """
        return height / width < 1/3 and not DISABLEPLAYLIST


    def albumArtWinWidth(self, height, width):
        """
        A function that calculates the album art window height and
        width based on the window height and width
        """
        if self.playlistFits(height, width) and not self.album_art_only:
            return height, round(width * 2/5)
        else:
            return height, width


    def fitText(self):
        """
        A function that fits album name, artist name and song name
        to the screen with the given width.
        """
        state = 0
        song = self.title
        album = self.album
        artist = self.artist
        width = self.art_window_width

        if len(song) > width:
            song = song[:width - len(song)]
            song = song[:-4].strip() + "..."

        if len(album) == 0:
            sep = 0
        else:
            sep = 3

        if len(artist) + len(album) + sep > width:
            state = 1
            if len(artist) > width:
                artist = artist[:width - len(artist)]
                artist = artist[:-4].strip() + "..."
            if len(album) > width:
                album = album[:width - len(album)]
                album = album[:-4].strip() + "..."

        if len(album) == 0:
            state = 2

        return (state, album, artist, song)


    def updateWindowSize(self, force_update=False):
        """
        A function to check if the window size changed
        """
        window_height, window_width = self.stdscr.getmaxyx()

        if (window_height, window_width) != (self.screen_height, self.screen_width) or force_update:

            self.draw_playlist = self.playlistFits(window_height, window_width) and not self.album_art_only

            # Album art window
            self.art_window_height, self.art_window_width = self.albumArtWinWidth(window_height, window_width)

            # Playlist window
            if self.draw_playlist:
                self.playlist_window_width  = window_width - self.art_window_width - PLAYLISTMARGIN
                self.playlist_window_height = window_height

            # Close the playlist window if it exists
            elif self.playlist_win is not None:
                del self.playlist_win
                self.playlist_win = None

            # Check if we are drawing info
            if self.album_art_only:
                self.text_start = int(self.art_window_height)
                self.album_space = self.text_start - 1
            else:
                self.text_start = int(self.art_window_height - 5)
                self.album_space = self.text_start - 2

            # Calculate the size of the image
            self.image_width_px, self.image_width, self.image_height = albumArtSize(self.album_space, self.art_window_width)
            self.image_y_pos = (self.album_space - self.image_height) // 2 + 1

            # Check if playlist window exists and if we are drawing it
            if self.playlist_win is not None and self.draw_playlist:
                self.playlist_win.clear()
                self.playlist_win.refresh()

                self.playlist_win.resize(
                    self.playlist_window_height,
                    self.playlist_window_width
                )

                self.playlist_win.mvwin(0, self.art_window_width + PLAYLISTMARGIN)

            elif self.draw_playlist:
                self.playlist_win = curses.newwin(
                    self.playlist_window_height, self.playlist_window_width,
                    0, self.art_window_width + PLAYLISTMARGIN
                )

            self.last_song = None

            # Resize the window
            self.art_win.clear()
            self.art_win.resize(self.art_window_height, self.art_window_width)

            self.screen_height, self.screen_width = window_height, window_width

    def getAlbumArt(self, song_file):
        """
        A function that fetches the album art and saves
        it to self.album_art_loc
        """
        http_base_url = art_config.get("http_base_url")

        if http_base_url:
            self._getAlbumArtFromHttpServer(http_base_url, song_file)
        else:
            self._getAlbumArtFromFile(song_file)



    def _getAlbumArtFromHttpServer(self, base_url, song_file):
        """
        A function that fetches the album art from the configured
        HTTP server, and saves it to self.album_art_loc
        """

        album = os.path.dirname(song_file)

        for cover_filename in art_config.get("http_cover_filenames", "cover.jpg").split():
            album_art_url = posixpath.join(base_url, album, cover_filename)

            try:
                album_art_resp = self.art_http_session.get(album_art_url)
            except requests.RequestException:
                # If any exception occurs, simply give up and show default art.
                self.drawDefaultAlbumArt()
                break

            if album_art_resp.ok:
                with open(self.album_art_loc, "wb") as f:
                    f.write(album_art_resp.content)
                break
            elif album_art_resp.status_code == 404:
                continue
        else:
            self.drawDefaultAlbumArt()


    def _getAlbumArtFromFile(self, song_file):
        """
        A function that extracts the album art from song_file and
        saves it to self.album_art_loc
        """
        music_dir = os.path.expanduser(
                art_config.get("music_directory", "~/Music"))

        song_file_abs = os.path.join(music_dir, song_file)

        process = (
            ffmpeg
            .input(song_file_abs)
            .output(self.album_art_loc)
        )

        try:
            process.run(quiet=True, overwrite_output=True)
        except ffmpeg._run.Error:
            self.drawDefaultAlbumArt()


    def drawDefaultAlbumArt(self):
        foregroundCol = "#D8DEE9"
        backgroundCol = "#262A33"

        size = 512*4

        art = Image.new("RGB", (size, size), color=backgroundCol)
        d = ImageDraw.Draw(art)

        for i in range(4):
            offset = (i - 2) * 70

            external = size/3

            x0 = round(external) - offset
            y0 = round(external) + offset
            x1 = round(external*2) - offset
            y1 = round(external*2) + offset

            externalyx = [(x0, y0), (x1, y1)]

            d.rectangle(externalyx, outline=foregroundCol, width=40)

        art.resize((512, 512))
        art.save(self.album_art_loc, "PNG")


    def getSongInfo(self, song):
        """
        A function that returns a tuple of the given songs
        album, artist, title
        if they do not exist, the function will return
        "", "", filename respectively
        """
        try:
            album = song["album"]
        except KeyError:
            album = ""

        try:
            artist = song["artist"]
        except KeyError:
            artist = ""

        try:
            title = song["title"]
        except KeyError:
            # If no title, use base file name
            aux = song["file"]
            aux = os.path.basename(aux)
            title = aux

        return album, artist, title


    def checkSongUpdate(self):
        """
        Checks if there is a new song playing

        Returns:
            1 -- if song state is "stop"
            0 -- if there is no change
            2 -- if there is a new song
        """
        status = self.client.status()

        if status["state"] == "stop":
            return 1

        song = self.client.currentsong()
        self.elapsed = float(status["elapsed"])
        self.duration = float(status["duration"])
        self.progress = self.elapsed/self.duration

        if self.last_song != song:
            self.art_win.clear()

            # Move selected_song to the currently playing one
            if self.control_cycle == 0:
                self.selected_song = int(song["pos"])

            self.album, self.artist, self.title = self.getSongInfo(song)
            self.getAlbumArt(song["file"])
            self.last_song = song
            
            # Update dominant color
            if self.auto_pairs:
                self.getDominantColor()

            return 0

        else:
            return 2


    def toggleInfo(self):
        """
        A function that toggles the display of track info
        """
        self.album_art_only = not self.album_art_only
        self.updateWindowSize(force_update=True)
        self.art_win.clear()
        self.art_win.refresh()


    def handleKeypress(self):
        """
        A function to handle keypresses

        Keys:
            '>' -- Next track
            '<' -- Last track
            '+' -- Volume up +5
            '-' -- Volume down -5
            'p' -- Play/pause
            'q' -- Quit
            'h' -- Help
        """

        anytime_keys = ["quit", "help", "select_up", "select_down", "select"]

        special_key_map = {curses.KEY_UP:     "up",
                           curses.KEY_DOWN:   "down",
                           curses.KEY_LEFT:   "left",
                           curses.KEY_RIGHT:  "right",
                           curses.KEY_ENTER:  "enter",
                           10:                "enter",
                           32:                "space"
                           }

        if self.checkSongUpdate() == 1:
            stopped = True
        else:
            stopped = False

        # Get key
        key = self.stdscr.getch()

        while key > 0:
            # Resolve every key in buffer
            if key in special_key_map.keys():
                keyChar = special_key_map[key]
            else:
                keyChar = chr(key).lower()

            # Get playlist length
            playlist_length = len(self.client.playlist())

            # Parse key
            if keyChar not in keybindings.keys():
                key = self.stdscr.getch()
                continue
            else:
                action = keybindings[keyChar]


            if stopped and action not in anytime_keys:
                key = self.stdscr.getch()
                continue

            if action == "next_track":
                self.client.next()
                self.update_needed = True

            elif action == "last_track":
                self.client.previous()
                self.update_needed = True

            elif action == "play_pause":
                self.client.pause()

            elif action == "volume_up":
                self.client.volume(str(VOLUMESTEP))

            elif action == "volume_down":
                self.client.volume(str(-VOLUMESTEP))

            elif action == "quit":
                raise KeyboardInterrupt

            elif action == "help":
                self.help = not self.help
                self.cleared = False
                self.update_needed = True

            elif action == "toggle_info":
                self.toggleInfo()
                self.update_needed = True

            elif action == "select_up":
                self.control_cycle = 1
                if playlist_length > 0:
                    self.selected_song = (self.selected_song - 1) % playlist_length
                self.update_needed = True

            elif action == "select_down":
                self.control_cycle = 1
                if playlist_length > 0:
                    self.selected_song = (self.selected_song + 1) % playlist_length
                self.update_needed = True

            elif action == "select":
                self.control_cycle = 1

                if playlist_length > 0:
                    self.client.play(self.selected_song % playlist_length)
                    self.update_needed = True
                    self.last_song = None


            key = self.stdscr.getch()

    def drawInfo(self):
        """
        A function to draw the info below the album art
        """
        state, album, artist, title = self.fitText()

        if len(self.artist) == 0:
            seperator = ""
        else:
            seperator = " - "

        if state == 0:
            # Everything fits
            self.art_win.addstr(self.text_start,     0, f"{title}")
            self.art_win.addstr(self.text_start + 1, 0, f"{artist}{seperator}{album}")

        elif state == 1:
            # Too wide
            self.art_win.addstr(self.text_start - 1, 0, f"{title}")
            self.art_win.addstr(self.text_start,     0, f"{album}")
            self.art_win.addstr(self.text_start + 1, 0, f"{artist}")

        else:
            # No album
            self.art_win.addstr(self.text_start,     0, f"{title}")
            self.art_win.addstr(self.text_start + 1, 0, f"{artist}")


        # Progress bar
        song_duration = (int(self.duration / 60), round(self.duration % 60))
        song_elapsed  = (int(self.elapsed / 60),  round(self.elapsed % 60))

        self.art_win.addstr(
            self.text_start + 2, 0,
            self.bar_body*(int((self.art_window_width - 1) * self.progress)) + self.bar_head,
            curses.color_pair(3)
        )

        # Duration string
        time_string = f"{song_elapsed[0]}:{song_elapsed[1]:02d}/{song_duration[0]}:{song_duration[1]:02d}"

        self.art_win.addstr(
            self.text_start + 3, 0,
            f"{time_string:>{self.art_window_width}}",
            curses.color_pair(2)
        )

        self.art_win.refresh()


    def drawPlaylist(self):
        """
        A function that draws the playlist
        """

        # Draw playlist
        if not self.draw_playlist:
            return

        playlist = self.client.playlistinfo()
        current_song = self.client.currentsong()

        # selected_pos = int(current_song["pos"])
        playlist_length = len(self.client.playlist())
        if playlist_length == 0:
            selected_pos = 0
            self.playlist_win.erase()
            self.playlist_win.refresh()
            return

        selected_pos = self.selected_song % len(playlist)

        # Determine where to start the playlist
        if selected_pos > self.playlist_window_height // 2 and len(playlist) > self.playlist_window_height:
            start = selected_pos - (self.playlist_window_height - 1) // 2
        else:
            start = 0

        start = min(abs(len(playlist) - self.playlist_window_height), start)

        line = 0
        while line < self.playlist_window_height:
            # Check if playlist is empty
            if line + start < len(playlist):
                playlist_item = playlist[start + line]
            else:
                playlist_item = None

            # Decide color
            pair = 0

            if playlist_item == current_song:
                pair = curses.color_pair(1)

            if playlist_item == playlist[selected_pos]:
                pair = curses.color_pair(1) | curses.A_REVERSE

            # Move and write text
            try:
                self.playlist_win.move(line, 0)

                if playlist_item is not None:
                    _, artist, title = self.getSongInfo(playlist_item)

                    if artist == "":
                        sep = ""
                    else:
                        sep = " - "

                    self.playlist_win.addstr(
                        f"{artist}{sep}{title}"[:self.playlist_window_width - 1],
                        pair
                    )

                self.playlist_win.clrtoeol()

            except curses.error:
                return

            line += 1

        self.playlist_win.refresh()


    def hideAlbumArt(self):
        """
        A function that hides the album art
        """
        if IMAGEMETHOD == "ueberzug":
            self.art_placement.visibility = ueberzug.Visibility.INVISIBLE


    def drawAlbumArt(self):
        """
        A function to draw the album art
        """

        if IMAGEMETHOD == "ueberzug":
            # Figure out new placement
            self.art_placement.x = (self.art_window_width - self.image_width)//2
            self.art_placement.y = self.image_y_pos

            # Figure out height and width
            self.art_placement.width = self.image_width
            self.art_placement.height = self.album_space

            # Update image
            self.art_placement.path = self.album_art_loc

            # Display image
            self.art_placement.visibility = ueberzug.Visibility.VISIBLE

        elif IMAGEMETHOD == "pixcat":
            (
                pixcat.Image(self.album_art_loc)
                .thumbnail(self.image_width_px)
                .show(x=(self.art_window_width - self.image_width)//2, y=self.image_y_pos)
            )


    def centerText(self, y: int, string: str):
        """
        A function that draws centered text in the window
        given a string and a line.

        Arguments:
            y      -- The y position to draw the string
            string -- The string to draw
        """

        x_pos = self.art_window_width / 2 - len(string) / 2
        self.art_win.addstr(y, int(x_pos), string)


    def drawHelp(self):
        """
        The function that draws the keymap help
        """

        # Top vspace
        top_vspace = 3

        # Left and right margin pct
        lr_margin_pct = 0.1
        lr_margin = round(self.art_window_width * lr_margin_pct)

        # Actual space for text
        x_space = self.art_window_width - 2 * (lr_margin)

        # Check if window has been cleared
        if not self.cleared:
            self.art_win.clear()
            self.cleared = True

        # Figure out center, y_start and x_start
        y_start = top_vspace
        x_start = int(lr_margin)

        # Draw title
        self.centerText(y_start, "Keymap")

        # Draw help
        for key, desc in keybindings.items():
            y_start += 1
            sep = "." * (x_space - len(key) - len(desc) - 2)
            desc = desc.replace("_", " ").capitalize()
            self.art_win.addstr(y_start, x_start, f"{key} {sep} {desc}")

        self.art_win.refresh()


    def draw(self):
        """
        The function that draws the now playing window
        """
        if not self.cleared:
            self.art_win.clear()
            self.cleared = True

        # Force window nings
        self.art_win.redrawln(0, 1)
        self.art_win.addstr(0, 0, " ")

        # Get mpd state
        state = self.checkSongUpdate()

        # Check if state is stop
        if state == 1:
            if self.has_music_been_played and AUTOCLOSE:
                # Check if the playlist has concluded and if we should close
                raise KeyboardInterrupt

            self.art_win.clear()
            self.hideAlbumArt()

            infomsg = "Put some beats on!"

            self.art_win.addstr(self.art_window_height // 2, (self.art_window_width - len(infomsg)) // 2, infomsg)
            self.art_win.refresh()
            self.drawPlaylist()

            return

        self.has_music_been_played = True

        # Draw the window
        if not self.album_art_only:
            self.drawInfo()
            self.drawPlaylist()

        self.drawAlbumArt()


    @ueberzug.Canvas()
    def loop(self, canvas):
        """
        The main program loop
        """

        if self.art_placement is None and IMAGEMETHOD == "ueberzug":
            # Create album art placement if we are using ueberzug
            self.art_placement = canvas.create_placement(
                "art",
                scaler=ueberzug.ScalerOption.FIT_CONTAIN.value
            )

        # Check if we need to recalculate window size
        # because of album art only initially
        if self.album_art_only:
            self.updateWindowSize(force_update=True)

        try:
            i = 0
            while True:
                s = time.perf_counter()

                self.handleKeypress()
                if i == 0 or self.update_needed:
                    # Checko for window size update
                    self.updateWindowSize()

                    if not self.help:
                        self.draw()

                    else:
                        self.hideAlbumArt()
                        self.drawHelp()

                    if self.color_update_needed:
                        self.getDominantColor()

                    self.update_needed = False

                # Update control_cycle once a second if it is not 0
                if i == 0 and self.control_cycle != 0:
                    self.control_cycle = (self.control_cycle + 1) % 30

                e = time.perf_counter()

                sleeptime = abs(1/FPS - (e-s))

                time.sleep(sleeptime)
                i = (i + 1) % FPS

        except KeyboardInterrupt:
            self.error = False or self.error
        except pixcat.terminal.KittyAnswerTimeout:
            self.error = "Kitty did not answer in time. Are you using Kitty?"
        finally:
            curses.nocbreak()
            curses.endwin()
            self.client.close()
            self.client.disconnect()
            if self.error:
                print(self.error)


try:
    player = Player()
    player.loop()
except ConnectionRefusedError:
    print(f"Could not connect to mpd on {MPDHOST}:{MPDPORT}")
finally:
    curses.nocbreak()
    curses.endwin()
