Merge pull request #11 from GuardKenzie/indev

Playlist view
This commit is contained in:
Tristan Ferrua
2021-05-13 01:55:32 +00:00
committed by GitHub
5 changed files with 302 additions and 102 deletions

View File

@ -35,6 +35,7 @@ The config file is located at `~/.config/miniplayer/config`. The example configu
* ***volume_step*:** The ammount (in percents) the volume will be adjusted on pressing the volume up and volume down keys. * ***volume_step*:** The ammount (in percents) the volume will be adjusted on pressing the volume up and volume down keys.
* ***album_art_only*:** Whether or not to only draw the album art and no other track info (`true/false`). * ***album_art_only*:** Whether or not to only draw the album art and no other track info (`true/false`).
* ***auto_close*:** Whether or not to automatically close the player once the mpd playlist has concluded (`true/false`). * ***auto_close*:** Whether or not to automatically close the player once the mpd playlist has concluded (`true/false`).
* ***show_playlist*:** Whether or not to show the playlist view.
#### mpd #### mpd
@ -44,7 +45,7 @@ The config file is located at `~/.config/miniplayer/config`. The example configu
#### keybindings #### keybindings
This section allows you to change the keybinds for the player. The format for a keybind is `key = action` (for example `p = play_pause`). Available actions are This section allows you to change the keybinds for the player. The format for a keybind is `key = action` (for example `p = play_pause` or `left = last_track`). Available actions are
* `play_pause` * `play_pause`
* `next_track` * `next_track`
* `last_track` * `last_track`
@ -53,20 +54,26 @@ This section allows you to change the keybinds for the player. The format for a
* `toggle_info` * `toggle_info`
* `help` * `help`
* `quit` * `quit`
* `select_down`
* `select_up`
* `select`
## Default keybinds ## Default keybinds
| Key | function | | Key | function |
|-----|---------------| |-------|---------------------|
| h | Show keybinds | | h | Show keybinds |
| p | Play/pause | | p | Play/pause |
| > | Next track | | > | Next track |
| < | Last track | | < | Last track |
| q | Quit | | q | Quit |
| + | Volume up | | + | Volume up |
| - | Volume down | | - | Volume down |
| i | Toggle info | | i | Toggle info |
| Up | Selection up |
| Down | Selection down |
| Enter | Play selected song |
These keybinds can be changed by editing the config file. See the [`config.example`](config.example) file for the format. These keybinds can be changed by editing the config file. See the [`config.example`](config.example) file for the format.
@ -78,3 +85,8 @@ If this does not work, try changing `image_method` from `pixcat` to `ueberzug` o
2. **Q:** Album art is too big/too small. 2. **Q:** Album art is too big/too small.
**A:** You need to configure `font_height` and `font_width`. Their values should be the actual pixel height and width of a character in your terminal. **A:** You need to configure `font_height` and `font_width`. Their values should be the actual pixel height and width of a character in your terminal.
## More screenshots!
![playlist](img/playlist.png)

View File

@ -2,7 +2,6 @@
import curses import curses
import os import os
from mpd import MPDClient from mpd import MPDClient
import mpd
import ffmpeg import ffmpeg
import pixcat import pixcat
import time import time
@ -21,7 +20,8 @@ if "player" not in config.sections():
"image_method": "pixcat", "image_method": "pixcat",
"album_art_only": False, "album_art_only": False,
"volume_step": 5, "volume_step": 5,
"auto_close": False "auto_close": False,
"show_playlist": True,
} }
if "mpd" not in config.sections(): if "mpd" not in config.sections():
@ -31,14 +31,17 @@ if "mpd" not in config.sections():
} }
# Initialise keybindings # Initialise keybindings
default_bindings = {">": "next_track", default_bindings = {">": "next_track",
"<": "last_track", "<": "last_track",
"+": "volume_up", "+": "volume_up",
"-": "volume_down", "-": "volume_down",
"p": "play_pause", "p": "play_pause",
"q": "quit", "q": "quit",
"h": "help", "h": "help",
"i": "toggle_info" "i": "toggle_info",
"down": "select_down",
"up": "select_up",
"enter": "select"
} }
if "keybindings" not in config.sections(): if "keybindings" not in config.sections():
@ -87,6 +90,12 @@ VOLUMESTEP = player_config.getint("volume_step", 5)
# Autoclose boolean # Autoclose boolean
AUTOCLOSE = player_config.getboolean("auto_close", False) 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): def albumArtSize(album_space, window_width):
""" """
@ -104,11 +113,15 @@ def albumArtSize(album_space, window_width):
return image_width_px, image_width, image_height return image_width_px, image_width, image_height
class Player: class Player:
def __init__(self): def __init__(self):
# Curses initialisation # Curses initialisation
self.stdscr = curses.initscr() self.stdscr = curses.initscr()
self.stdscr.nodelay(True) self.stdscr.nodelay(True)
self.stdscr.keypad(True)
# Curses config # Curses config
curses.noecho() curses.noecho()
@ -129,15 +142,39 @@ class Player:
self.last_song = None self.last_song = None
# Curses window # Album art only flag
self.window_height, self.window_width = self.stdscr.getmaxyx() self.album_art_only = player_config.getboolean("album_art_only", False)
self.win = curses.newwin(self.window_height, self.window_width, 0, 0)
self.text_start = int(self.window_height - 5) # 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 self.album_space = self.text_start - 2
# Calculate the size of the image # Calculate the size of the image
self.image_width_px, self.image_width, self.image_height = albumArtSize(self.album_space, self.window_width) 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 self.image_y_pos = (self.album_space - self.image_height) // 2 + 1
# Album art location # Album art location
@ -153,12 +190,34 @@ class Player:
# Update needed flag # Update needed flag
self.update_needed = False self.update_needed = False
# Album art only flag
self.album_art_only = player_config.getboolean("album_art_only", False)
# Flag to check if any music has been played # Flag to check if any music has been played
self.has_music_been_played = False 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 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): def fitText(self):
""" """
@ -169,7 +228,7 @@ class Player:
song = self.title song = self.title
album = self.album album = self.album
artist = self.artist artist = self.artist
width = self.window_width width = self.art_window_width
if len(song) > width: if len(song) > width:
song = song[:width - len(song)] song = song[:width - len(song)]
@ -199,30 +258,63 @@ class Player:
""" """
A function to check if the window size changed A function to check if the window size changed
""" """
new_height, new_width = self.stdscr.getmaxyx() window_height, window_width = self.stdscr.getmaxyx()
if (new_height, new_width) != (self.window_height, self.window_width) or force_update: if (window_height, window_width) != (self.screen_height, self.screen_width) or force_update:
self.win.clear()
# Curses window self.draw_playlist = self.playlistFits(window_height, window_width) and not self.album_art_only
self.window_height, self.window_width = self.stdscr.getmaxyx()
# 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 # Check if we are drawing info
if self.album_art_only: if self.album_art_only:
self.text_start = int(self.window_height) self.text_start = int(self.art_window_height)
self.album_space = self.text_start - 1 self.album_space = self.text_start - 1
else: else:
self.text_start = int(self.window_height - 5) self.text_start = int(self.art_window_height - 5)
self.album_space = self.text_start - 2 self.album_space = self.text_start - 2
# Calculate the size of the image # Calculate the size of the image
self.image_width_px, self.image_width, self.image_height = albumArtSize(self.album_space, self.window_width) 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 self.image_y_pos = (self.album_space - self.image_height) // 2 + 1
# Resize the window # Check if playlist window exists and if we are drawing it
self.win.resize(self.window_height, self.window_width) 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 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): def getAlbumArt(self, song_file):
""" """
@ -233,9 +325,9 @@ class Player:
song_file_abs = os.path.join(MUSICDIR, song_file) song_file_abs = os.path.join(MUSICDIR, song_file)
process = ( process = (
ffmpeg ffmpeg
.input(song_file_abs) .input(song_file_abs)
.output(self.album_art_loc) .output(self.album_art_loc)
) )
try: try:
@ -261,13 +353,12 @@ class Player:
externalyx = [(x0, y0), (x1, y1)] externalyx = [(x0, y0), (x1, y1)]
d.rectangle(externalyx, outline=foregroundCol, width=40)# fill=foregroundCol) d.rectangle(externalyx, outline=foregroundCol, width=40)
# d.ellipse(internalxy, fill=backgroundCol)
art.resize((512, 512)) art.resize((512, 512))
art.save(self.album_art_loc, "PNG") art.save(self.album_art_loc, "PNG")
def checkSongUpdate(self): def checkSongUpdate(self):
""" """
Checks if there is a new song playing Checks if there is a new song playing
@ -288,7 +379,11 @@ class Player:
self.progress = self.elapsed/self.duration self.progress = self.elapsed/self.duration
if self.last_song != song: if self.last_song != song:
self.win.clear() self.art_win.clear()
# Move selected_song to the currently playing one
if self.control_cycle == 0:
self.selected_song = int(song["pos"])
try: try:
self.album = song["album"] self.album = song["album"]
@ -324,11 +419,10 @@ class Player:
""" """
A function that toggles the display of track info A function that toggles the display of track info
""" """
self.album_art_only = not self.album_art_only self.album_art_only = not self.album_art_only
self.win.clear()
self.updateWindowSize(force_update=True) self.updateWindowSize(force_update=True)
self.win.refresh() self.art_win.clear()
self.art_win.refresh()
def handleKeypress(self): def handleKeypress(self):
@ -345,7 +439,15 @@ class Player:
'h' -- Help 'h' -- Help
""" """
anytime_keys = ["quit", "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"
}
if self.checkSongUpdate() == 1: if self.checkSongUpdate() == 1:
stopped = True stopped = True
@ -357,7 +459,10 @@ class Player:
while key > 0: while key > 0:
# Resolve every key in buffer # Resolve every key in buffer
keyChar = chr(key).lower() if key in special_key_map.keys():
keyChar = special_key_map[key]
else:
keyChar = chr(key).lower()
# Parse key # Parse key
if keyChar not in keybindings.keys(): if keyChar not in keybindings.keys():
@ -400,6 +505,22 @@ class Player:
self.toggleInfo() self.toggleInfo()
self.update_needed = True self.update_needed = True
elif action == "select_up":
self.control_cycle = 1
self.selected_song -= 1
self.update_needed = True
elif action == "select_down":
self.control_cycle = 1
self.selected_song += 1
self.update_needed = True
elif action == "select":
self.control_cycle = 1
self.client.play(self.selected_song % len(self.client.playlist()))
self.update_needed = True
key = self.stdscr.getch() key = self.stdscr.getch()
def drawInfo(self): def drawInfo(self):
@ -415,41 +536,101 @@ class Player:
if state == 0: if state == 0:
# Everything fits # Everything fits
self.win.addstr(self.text_start, 0, f"{title}") self.art_win.addstr(self.text_start, 0, f"{title}")
self.win.addstr(self.text_start + 1, 0, f"{artist}{seperator}{album}") self.art_win.addstr(self.text_start + 1, 0, f"{artist}{seperator}{album}")
elif state == 1: elif state == 1:
# Too wide # Too wide
self.win.addstr(self.text_start - 1, 0, f"{title}") self.art_win.addstr(self.text_start - 1, 0, f"{title}")
self.win.addstr(self.text_start, 0, f"{album}") self.art_win.addstr(self.text_start, 0, f"{album}")
self.win.addstr(self.text_start + 1, 0, f"{artist}") self.art_win.addstr(self.text_start + 1, 0, f"{artist}")
else: else:
# No album # No album
self.win.addstr(self.text_start, 0, f"{title}") self.art_win.addstr(self.text_start, 0, f"{title}")
self.win.addstr(self.text_start + 1, 0, f"{artist}") self.art_win.addstr(self.text_start + 1, 0, f"{artist}")
# Progress bar # Progress bar
song_duration = (int(self.duration / 60), round(self.duration % 60)) song_duration = (int(self.duration / 60), round(self.duration % 60))
song_elapsed = (int(self.elapsed / 60), round(self.elapsed % 60)) song_elapsed = (int(self.elapsed / 60), round(self.elapsed % 60))
self.win.addstr( self.art_win.addstr(
self.text_start + 2, 0, self.text_start + 2, 0,
"-"*(int((self.window_width - 1) * self.progress)) + ">", "-"*(int((self.art_window_width - 1) * self.progress)) + ">",
curses.color_pair(1) curses.color_pair(1)
) )
# Duration string # Duration string
time_string = f"{song_elapsed[0]}:{song_elapsed[1]:02d}/{song_duration[0]}:{song_duration[1]:02d}" time_string = f"{song_elapsed[0]}:{song_elapsed[1]:02d}/{song_duration[0]}:{song_duration[1]:02d}"
self.win.addstr( self.art_win.addstr(
self.text_start + 3, 0, self.text_start + 3, 0,
f"{time_string:>{self.window_width}}", f"{time_string:>{self.art_window_width}}",
curses.color_pair(2) curses.color_pair(2)
) )
self.win.refresh() 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"])
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(2)
if playlist_item == playlist[selected_pos]:
pair = curses.color_pair(2) | curses.A_REVERSE
# Move and write text
try:
self.playlist_win.move(line, 0)
if playlist_item is not None:
self.playlist_win.addstr(
f"{playlist_item['artist']} - {playlist_item['title']}"[:self.playlist_window_width - 1],
pair
)
self.playlist_win.clrtoeol()
except curses.error:
return
line += 1
self.playlist_win.refresh()
def hideAlbumArt(self): def hideAlbumArt(self):
@ -457,7 +638,7 @@ class Player:
A function that hides the album art A function that hides the album art
""" """
if IMAGEMETHOD == "ueberzug": if IMAGEMETHOD == "ueberzug":
self.art_placement.visibility = ueberzug.Visibility.INVISIBLE self.art_placement.visibility = ueberzug.Visibility.INVISIBLE
def drawAlbumArt(self): def drawAlbumArt(self):
@ -467,7 +648,7 @@ class Player:
if IMAGEMETHOD == "ueberzug": if IMAGEMETHOD == "ueberzug":
# Figure out new placement # Figure out new placement
self.art_placement.x = (self.window_width - self.image_width)//2 self.art_placement.x = (self.art_window_width - self.image_width)//2
self.art_placement.y = self.image_y_pos self.art_placement.y = self.image_y_pos
# Figure out height and width # Figure out height and width
@ -483,8 +664,8 @@ class Player:
elif IMAGEMETHOD == "pixcat": elif IMAGEMETHOD == "pixcat":
( (
pixcat.Image(self.album_art_loc) pixcat.Image(self.album_art_loc)
.thumbnail(self.image_width_px ) .thumbnail(self.image_width_px)
.show(x=(self.window_width - self.image_width)//2, y=self.image_y_pos) .show(x=(self.art_window_width - self.image_width)//2, y=self.image_y_pos)
) )
@ -498,8 +679,8 @@ class Player:
string -- The string to draw string -- The string to draw
""" """
x_pos = self.window_width / 2 - len(string) / 2 x_pos = self.art_window_width / 2 - len(string) / 2
self.win.addstr(y, int(x_pos), string) self.art_win.addstr(y, int(x_pos), string)
def drawHelp(self): def drawHelp(self):
@ -512,18 +693,17 @@ class Player:
# Left and right margin pct # Left and right margin pct
lr_margin_pct = 0.1 lr_margin_pct = 0.1
lr_margin = round(self.window_width * lr_margin_pct) lr_margin = round(self.art_window_width * lr_margin_pct)
# Actual space for text # Actual space for text
x_space = self.window_width - 2 * (lr_margin) x_space = self.art_window_width - 2 * (lr_margin)
# Check if window has been cleared # Check if window has been cleared
if not self.cleared: if not self.cleared:
self.win.clear() self.art_win.clear()
self.cleared = True self.cleared = True
# Figure out center, y_start and x_start # Figure out center, y_start and x_start
center_y, center_x = (self.window_height // 2, self.window_width // 2)
y_start = top_vspace y_start = top_vspace
x_start = int(lr_margin) x_start = int(lr_margin)
@ -535,9 +715,9 @@ class Player:
y_start += 1 y_start += 1
sep = "." * (x_space - len(key) - len(desc) - 2) sep = "." * (x_space - len(key) - len(desc) - 2)
desc = desc.replace("_", " ").capitalize() desc = desc.replace("_", " ").capitalize()
self.win.addstr(y_start, x_start, f"{key} {sep} {desc}") self.art_win.addstr(y_start, x_start, f"{key} {sep} {desc}")
self.win.refresh() self.art_win.refresh()
def draw(self): def draw(self):
@ -545,12 +725,12 @@ class Player:
The function that draws the now playing window The function that draws the now playing window
""" """
if not self.cleared: if not self.cleared:
self.win.clear() self.art_win.clear()
self.cleared = True self.cleared = True
# Force window nings # Force window nings
self.win.redrawln(0, 1) self.art_win.redrawln(0, 1)
self.win.addstr(0, 0, "") self.art_win.addstr(0, 0, "")
# Get mpd state # Get mpd state
state = self.checkSongUpdate() state = self.checkSongUpdate()
@ -561,13 +741,14 @@ class Player:
# Check if the playlist has concluded and if we should close # Check if the playlist has concluded and if we should close
raise KeyboardInterrupt raise KeyboardInterrupt
self.win.clear() self.art_win.clear()
self.hideAlbumArt() self.hideAlbumArt()
infomsg = "Put some beats on!" infomsg = "Put some beats on!"
self.win.addstr(self.window_height // 2, (self.window_width - len(infomsg)) // 2, infomsg) self.art_win.addstr(self.art_window_height // 2, (self.art_window_width - len(infomsg)) // 2, infomsg)
self.win.refresh() self.art_win.refresh()
self.drawPlaylist()
return return
@ -576,6 +757,7 @@ class Player:
# Draw the window # Draw the window
if not self.album_art_only: if not self.album_art_only:
self.drawInfo() self.drawInfo()
self.drawPlaylist()
self.drawAlbumArt() self.drawAlbumArt()
@ -617,6 +799,10 @@ class Player:
self.update_needed = False 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() e = time.perf_counter()
sleeptime = abs(1/FPS - (e-s)) sleeptime = abs(1/FPS - (e-s))
@ -644,6 +830,3 @@ except ConnectionRefusedError:
curses.nocbreak() curses.nocbreak()
curses.endwin() curses.endwin()
print(f"Could not connect to mpd on {MPDHOST}:{MPDPORT}") print(f"Could not connect to mpd on {MPDHOST}:{MPDPORT}")

View File

@ -1,23 +1,28 @@
[player] [player]
music_directory = ~/Music music_directory = ~/Music
font_width = 11 font_width = 11
font_height = 24 font_height = 24
image_method = pixcat image_method = pixcat
volume_step = 5 volume_step = 5
auto_close = false auto_close = false
album_art_only = false album_art_only = false
show_playlist = true
[mpd] [mpd]
host = localhost host = localhost
port = 6600 port = 6600
# pass = example # pass = example
# [keybindings] # [keybindings]
# > = next_track # > = next_track
# < = last_track # < = last_track
# + = volume_up # + = volume_up
# - = volume_down # - = volume_down
# p = play_pause # p = play_pause
# q = quit # q = quit
# h = help # h = help
# i = toggle_info # i = toggle_info
# up = select_up
# down = select_down
# enter = select

BIN
img/playlist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = miniplayer name = miniplayer
version = 1.2.0 version = 1.3.0
description = An mpd client with album art and basic functionality. description = An mpd client with album art and basic functionality.
long_description = file: README.md long_description = file: README.md
long_description_content_type = text/markdown long_description_content_type = text/markdown