From 8a79cafdddb0fb7eebde1b9652bbe798e837004b Mon Sep 17 00:00:00 2001 From: Raphael McSinyx Date: Mon, 3 Apr 2017 20:52:59 +0700 Subject: [PATCH] Add repeat and shuffle mode --- comp.py | 184 +++++++++++++++++++++++++++++++-------------------- settings.ini | 9 +++ setup.py | 4 +- 3 files changed, 122 insertions(+), 75 deletions(-) create mode 100644 settings.ini diff --git a/comp.py b/comp.py index 330d84b..0966f52 100755 --- a/comp.py +++ b/comp.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 - + # comp - Curses Online Media Player # Copyright (C) 2017 Raphael McSinyx # @@ -20,11 +20,17 @@ import curses import json from argparse import ArgumentParser from configparser import ConfigParser +from itertools import cycle from os.path import expanduser +from random import choice from time import gmtime, strftime +from threading import Thread from mpv import MPV +MODES = ('play-current', 'play-all', 'play-selected', 'repeat-current', + 'repeat-all', 'repeat-selected', 'shuffle-all', 'shuffle-selected') + def setno(data, keys): """Set all keys of each track in data to False.""" @@ -33,30 +39,27 @@ def setno(data, keys): track[key] = False -def find(data, key): - """Return the list tracks with key set to True.""" - return [track for track in data if track[key]] +def playlist(mode): + """Return a generator of tracks to be played.""" + action, choose_from = mode.split('-') + if choose_from == 'all': tracks = data + else: tracks = [track for track in data if track[choose_from]] + # Somehow yield have to be used instead of returning a generator + if action == 'play': + for track in tracks: yield track + elif action == 'repeat': + for track in cycle(tracks): yield track + elif tracks: + while True: yield choice(tracks) -def initmpv(vf, af): - """Return a mpv object with youtube-dl video format set to vf+af.""" - ytdlf = '{}+{}'.format(vf, af) if vf and af else vf + af - mp = MPV(input_default_bindings=True, input_vo_keyboard=True, - ytdl=True, ytdl_format=ytdlf) - if video_output: mp['vo'] = video_output - mp.observe_property('pause', lambda foo: updatestatusline(stdscr, mp)) - mp.observe_property('time-pos', lambda foo: updatestatusline(stdscr, mp)) - mp.observe_property('aid', lambda foo: updatestatusline(stdscr, mp)) - mp.observe_property('vid', lambda foo: updatestatusline(stdscr, mp)) - return mp - - -def play(track): - setno(data, ['playing']) - mp._set_property('pause', False, bool) - mp.play('https://youtu.be/' + track['url']) - track['playing'] = True - reprint(stdscr, data[start : start+curses.LINES-3]) +def play(): + for track in playlist(mode): + setno(data, ['playing']) + data[data.index(track)]['playing'] = True + reprint(stdscr, data[start : start+curses.LINES-3]) + mp.play('https://youtu.be/' + track['url']) + mp.wait_for_playback() def secpair2hhmmss(pos, duration): @@ -71,23 +74,41 @@ def secpair2hhmmss(pos, duration): strftime(timestr, durationtime)) -def updatestatusline(stdscr, mp): - playmode = ' {}{} {}'.format('A' if mp._get_property('aid') != 'no' else ' ', - 'V' if mp._get_property('vid') != 'no' else ' ', - mode) - time = secpair2hhmmss(mp._get_property('time-pos', int), - mp._get_property('duration', int)) - if time: - stdscr.addstr(curses.LINES - 2, 1, '{} {} {}'.format( - time, - '|' if mp._get_property('pause', bool) else '>', - mp._get_property('media-title') - )[:curses.COLS-len(playmode)]) - stdscr.addstr(curses.LINES - 2, curses.COLS - 1 - len(playmode), playmode) - stdscr.chgat(curses.LINES - 2, 0, curses.color_pair(8)) +def update_status_line(stdscr, mp): + left = ' ' + secpair2hhmmss(mp._get_property('time-pos', int), + mp._get_property('duration', int)) + right = ' {} {}{} '.format(mode, + ' ' if mp._get_property('mute', bool) else 'A', + ' ' if mp._get_property('vid') == 'no' else 'V') + if left != ' ': + left += ' | ' if mp._get_property('pause', bool) else ' > ' + stdscr.addstr(curses.LINES - 2, 0, left, curses.color_pair(8)) + title_len = curses.COLS - len(left + right) + center = mp._get_property('media-title').ljust(title_len)[:title_len] + stdscr.addstr(curses.LINES - 2, len(left), center, + curses.color_pair(8) | curses.A_BOLD) + stdscr.addstr(curses.LINES - 2, len(left + center), right, + curses.color_pair(8)) + else: + stdscr.addstr(curses.LINES - 2, 0, right.rjust(curses.COLS), + curses.color_pair(8)) stdscr.refresh() +def reattr(stdscr, y, track): + invert = 8 if track['current'] else 0 + if track['error']: + stdscr.chgat(y, 0, curses.color_pair(1 + invert) | curses.A_BOLD) + elif track['playing']: + stdscr.chgat(y, 0, curses.color_pair(3 + invert) | curses.A_BOLD) + elif track['selected']: + stdscr.chgat(y, 0, curses.color_pair(5 + invert) | curses.A_BOLD) + elif invert: + stdscr.chgat(y, 0, curses.color_pair(12) | curses.A_BOLD) + else: + stdscr.chgat(y, 0, curses.color_pair(0) | curses.A_NORMAL) + + def reprint(stdscr, data2print): stdscr.clear() stdscr.addstr(0, curses.COLS-12, 'URL') @@ -97,48 +118,49 @@ def reprint(stdscr, data2print): y = i + 1 stdscr.addstr(y, 0, track['url'].rjust(curses.COLS - 1)) stdscr.addstr(y, 1, track['title'][:curses.COLS-14]) - invert = 8 if track['highlight'] else 0 - if track['error']: - stdscr.chgat(y, 0, curses.color_pair(1 + invert) | curses.A_BOLD) - elif track['playing']: - stdscr.chgat(y, 0, curses.color_pair(3 + invert) | curses.A_BOLD) - elif track['selected']: - stdscr.chgat(y, 0, curses.color_pair(5 + invert) | curses.A_BOLD) - elif invert: - stdscr.chgat(y, 0, curses.color_pair(12) | curses.A_BOLD) - else: - stdscr.chgat(y, 0, curses.color_pair(0) | curses.A_NORMAL) - updatestatusline(stdscr, mp) + reattr(stdscr, y, track) + update_status_line(stdscr, mp) def move(stdscr, data, y, delta): global start if start + y + delta < 1: + if start + y == 1: + return 1 start = 0 - setno(data, ['highlight']) - data[0]['highlight'] = True + setno(data, ['current']) + data[0]['current'] = True reprint(stdscr, data[:curses.LINES-3]) return 1 elif start + y + delta > len(data): + if start + y == len(data): + return curses.LINES - 3 start = len(data) - curses.LINES + 3 y = curses.LINES - 3 - setno(data, ['highlight']) - data[-1]['highlight'] = True + setno(data, ['current']) + data[-1]['current'] = True reprint(stdscr, data[-curses.LINES+3:]) return y - if 0 < y + delta < curses.LINES - 2: - y = y + delta - elif y + delta < 1: + if y + delta < 1: start += y + delta - 1 y = 1 - else: + setno(data, ['current']) + data[start]['current'] = True + reprint(stdscr, data[start : start+curses.LINES-3]) + elif y + delta > curses.LINES - 3: start += y + delta - curses.LINES + 3 y = curses.LINES - 3 - setno(data, ['highlight']) - data[start + y - 1]['highlight'] = True - reprint(stdscr, data[start : start+curses.LINES-3]) - stdscr.refresh() + setno(data, ['current']) + data[start + curses.LINES - 4]['current'] = True + reprint(stdscr, data[start : start+curses.LINES-3]) + else: + data[start + y - 1]['current'] = False + reattr(stdscr, y, data[start + y - 1]) + y = y + delta + data[start + y - 1]['current'] = True + reattr(stdscr, y, data[start + y - 1]) + stdscr.refresh() return y @@ -150,14 +172,13 @@ args = parser.parse_args() config = ConfigParser() config.read(expanduser('~/.config/comp/settings.ini')) mode = config.get('comp', 'play-mode', fallback='play-all') +video = config.get('mpv', 'video', fallback='auto') video_output = config.get('mpv', 'video-output', fallback='') -audio_format = config.get('youtube-dl', 'audio-format', fallback='bestaudio') -video_format = config.get('youtube-dl', 'video-format', fallback='bestvideo') -audio, video = audio_format, video_format +ytdlf = config.get('youtube-dl', 'format', fallback='best') with open(args.json_playlist) as f: data = json.load(f) -setno(data, ['error', 'playing', 'selected', 'highlight']) +setno(data, ['error', 'playing', 'selected', 'current']) stdscr = curses.initscr() curses.noecho() @@ -181,12 +202,19 @@ curses.init_pair(12, -1, 4) curses.init_pair(13, -1, 5) curses.init_pair(14, -1, 6) -mp = initmpv(video, audio) +mp = MPV(input_default_bindings=True, input_vo_keyboard=True, + ytdl=True, ytdl_format=ytdlf) +if video_output: mp['vo'] = video_output +mp._set_property('vid', video) +mp.observe_property('mute', lambda foo: update_status_line(stdscr, mp)) +mp.observe_property('pause', lambda foo: update_status_line(stdscr, mp)) +mp.observe_property('time-pos', lambda foo: update_status_line(stdscr, mp)) +mp.observe_property('vid', lambda foo: update_status_line(stdscr, mp)) # Print initial content start = 0 y = 1 -data[0]['highlight'] = True +data[0]['current'] = True reprint(stdscr, data[:curses.LINES-3]) # mpv keys: []{}<>.,qQ/*90m-#fTweoPOvjJxzlLVrtsSIdA @@ -195,29 +223,39 @@ c = stdscr.getch() while c != 113: # letter q if c == curses.KEY_RESIZE: curses.update_lines_cols() - move(stdscr, data, y, 1 - y) - y = move(stdscr, data, 1, y - 1) + start += y - 1 + y = 1 + reprint(stdscr, data[start : start+curses.LINES-3]) elif c in (106, curses.KEY_DOWN): # letter j or down arrow y = move(stdscr, data, y, 1) elif c in (107, curses.KEY_UP): # letter k or up arrow y = move(stdscr, data, y, -1) elif c == curses.KEY_PPAGE: # page up - y = move(stdscr, data, y, -curses.LINES) + y = move(stdscr, data, y, 4 - curses.LINES) elif c == curses.KEY_NPAGE: # page down - y = move(stdscr, data, y, curses.LINES) + y = move(stdscr, data, y, curses.LINES - 4) elif c == curses.KEY_HOME: # home y = move(stdscr, data, y, -len(data)) elif c == curses.KEY_END: # end y = move(stdscr, data, y, len(data)) + elif c == 109: # letter m + mode = MODES[(MODES.index(mode) + 1) % 8] + update_status_line(stdscr, mp) + elif c == 77: # letter M + mode = MODES[(MODES.index(mode) - 1) % 8] + update_status_line(stdscr, mp) elif c == 112: # letter p - play(data[start + y - 1]) + mp._set_property('pause', False, bool) + play_thread = Thread(target=play) + play_thread.daemon = True + play_thread.start() elif c == 32: # space mp._set_property('pause', not mp._get_property('pause', bool), bool) elif c == 99: # letter c data[start + y - 1]['selected'] = not data[start + y - 1]['selected'] y = move(stdscr, data, y, 1) elif c == 97: # letter a - mp._set_property('aid', 'auto' if mp._get_property('aid') == 'no' else 'no') + mp._set_property('mute', not mp._get_property('mute', bool), bool) elif c == 118: # letter v mp._set_property('vid', 'auto' if mp._get_property('vid') == 'no' else 'no') c = stdscr.getch() diff --git a/settings.ini b/settings.ini new file mode 100644 index 0000000..35d774b --- /dev/null +++ b/settings.ini @@ -0,0 +1,9 @@ +[comp] +play-mode = shuffle-selected + +[mpv] +video = no +video-output = xv + +[youtube-dl] +format = best diff --git a/setup.py b/setup.py index 5c2346b..f2156c4 100755 --- a/setup.py +++ b/setup.py @@ -5,14 +5,14 @@ from distutils.core import setup with open('README.rst') as f: long_description = f.read() -setup(name = 'comp', version = '0.1.0a2', +setup(name = 'comp', version = '0.1.0a3', url = 'https://github.com/McSinyx/comp', description = ('Curses Online Media Player'), long_description=long_description, author = 'McSinyx', author_email = 'vn.mcsinyx@gmail.com', py_modules = ['mpv'], scripts=['comp.py'], classifiers = [ - 'Development Status :: 2 - Pre-alpha', + 'Development Status :: 3 - Alpha', 'Environment :: Console :: Curses', 'Intended Audience :: End Users/Desktop', 'License :: OSI Approved :: GNU Affero General Public License v3',