Add search function

This commit is contained in:
Nguyễn Gia Phong 2017-05-07 15:59:54 +07:00
parent 1cd22a5ce9
commit a038e355e7
4 changed files with 324 additions and 202 deletions

View file

@ -38,16 +38,29 @@ Usage
:: ::
$ comp --help $ comp --help
usage: comp [-h] [-j JSON_PLAYLIST] usage: comp [-h] [-c CONFIG] [--vid {ID,auto,no}] [--vo DRIVER]
[-f YTDL_FORMAT] [-u URL] [-j JSON_PLAYLIST]
Curses Online Media Player Curses Online Media Player
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-j JSON_PLAYLIST, --json-playlist JSON_PLAYLIST -c CONFIG, --config CONFIG
path to playlist in JSON format location of the configuration file; either the path
-y YOUTUBE_PLAYLIST, --youtube-playlist YOUTUBE_PLAYLIST to the config or its containing directory
URL to an playlist on Youtube --vid {ID,auto,no} initial video channel. auto selects the default, no
disables video
--vo DRIVER specify the video output backend to be used. See
VIDEO OUTPUT DRIVERS in mpv(1) man page for details
and descriptions of available drivers
-f YTDL_FORMAT, --format YTDL_FORMAT
video format/quality to be passed to youtube-dl
-u URL, --online-playlist URL
URL to an playlist on Youtube
-j JSON_PLAYLIST, --json JSON_PLAYLIST
path to playlist in JSON format. If
--online-playlist is already specified, this will be
used as the default file to save the playlist
Keyboard control Keyboard control
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
@ -59,19 +72,25 @@ Keyboard control
+--------------+---------------------------------------------+ +--------------+---------------------------------------------+
| Space | Select the current track | | Space | Select the current track |
+--------------+---------------------------------------------+ +--------------+---------------------------------------------+
| ``/``, ``?`` | Search forward/backward for a pattern |
+--------------+---------------------------------------------+
| ``<``, ``>`` | Go forward/backward in the playlist | | ``<``, ``>`` | Go forward/backward in the playlist |
+--------------+---------------------------------------------+ +--------------+---------------------------------------------+
| ``A`` | Toggle mute | | ``A`` | Toggle mute |
+--------------+---------------------------------------------+ +--------------+---------------------------------------------+
| ``N`` | Repeat previous search in reverse direction |
+--------------+---------------------------------------------+
| ``U`` | Open online playlist | | ``U`` | Open online playlist |
+--------------+---------------------------------------------+ +--------------+---------------------------------------------+
| ``V`` | Toggle video | | ``V`` | Toggle video |
+--------------+---------------------------------------------+ +--------------+---------------------------------------------+
| ``W`` | Save the current playlist under JSON format | | ``W`` | Save the current playlist under JSON format |
+--------------+---------------------------------------------+ +--------------+---------------------------------------------+
| ``d`` | Delete current entry |
+--------------+---------------------------------------------+
| ``m``, ``M`` | Cycle through playing modes | | ``m``, ``M`` | Cycle through playing modes |
+--------------+---------------------------------------------+ +--------------+---------------------------------------------+
| ``d`` | Delete current entry | | ``n`` | Repeat previous search |
+--------------+---------------------------------------------+ +--------------+---------------------------------------------+
| ``p`` | Toggle pause | | ``p`` | Toggle pause |
+--------------+---------------------------------------------+ +--------------+---------------------------------------------+
@ -104,17 +123,19 @@ user-specific one is ``~/.config/mpv/settings.ini``. Default configurations
are listed below:: are listed below::
[comp] [comp]
# Supported 8 modes: play-current, play-all, play-selected, repeat-current, # Initial playing mode, which can be one of these 8 modes: play-current,
# repeat-all, repeat-selected, shuffle-all and shuffle-selected. # play-all, play-selected, repeat-current, repeat-all, repeat-selected,
# shuffle-all and shuffle-selected.
play-mode = play-current play-mode = play-current
[mpv] [mpv]
# Set if video should be download and play, I only know 2 possible values: # Initial video channel. auto selects the default, no disables video.
# auto and no. This can be changed later interactively.
video = auto video = auto
# Read more on VIDEO OUTPUT DRIVERS section in mpv man page. # Specify the video output backend to be used. See VIDEO OUTPUT DRIVERS in
# mpv(1) man page for details and descriptions of available drivers.
video-output = video-output =
[youtube-dl] [youtube-dl]
# Read more on FORMAT SELECTION section in youtube-dl man page. # Video format/quality to be passed to youtube-dl. See FORMAT SELECTION in
# youtube-dl(1) man page for more details and descriptions.
format = best format = best

455
comp
View file

@ -18,7 +18,9 @@
import curses import curses
import json import json
import re
from argparse import ArgumentParser from argparse import ArgumentParser
from collections import deque
from configparser import ConfigParser from configparser import ConfigParser
from curses.ascii import ctrl from curses.ascii import ctrl
from datetime import datetime from datetime import datetime
@ -26,7 +28,7 @@ from functools import reduce
from gettext import gettext as _, textdomain from gettext import gettext as _, textdomain
from itertools import cycle from itertools import cycle
from os import linesep, makedirs from os import linesep, makedirs
from os.path import abspath, dirname, expanduser, isfile from os.path import abspath, dirname, expanduser, isdir, isfile, join
from random import choice from random import choice
from time import gmtime, strftime from time import gmtime, strftime
from threading import Thread from threading import Thread
@ -42,9 +44,10 @@ textdomain('comp')
SYSTEM_CONFIG = '/etc/comp/settings.ini' SYSTEM_CONFIG = '/etc/comp/settings.ini'
USER_CONFIG = expanduser('~/.config/comp/settings.ini') USER_CONFIG = expanduser('~/.config/comp/settings.ini')
MPV_LOG = expanduser('~/.cache/comp/mpv.log') MPV_LOG = expanduser('~/.cache/comp/mpv.log')
MODES = ('play-current', 'play-all', 'play-selected', 'repeat-current', MODES = ("play-current", "play-all", "play-selected", "repeat-current",
'repeat-all', 'repeat-selected', 'shuffle-all', 'shuffle-selected') "repeat-all", "repeat-selected", "shuffle-all", "shuffle-selected")
MODE_STR_LEN = max(len(_(mode)) for mode in MODES) MODE_STR_LEN = max(len(_(mode)) for mode in MODES)
DURATION_COL_LEN = max(len(_("Duration")), 8)
def mpv_logger(loglevel, component, message): def mpv_logger(loglevel, component, message):
@ -54,12 +57,18 @@ def mpv_logger(loglevel, component, message):
f.write(mpv_log) f.write(mpv_log)
def justified(s, width):
"""Return s left-justified of length width."""
return s.ljust(width)[:width]
class Comp(object): class Comp(object):
"""Meta object for drawing and playing. """Meta object for drawing and playing.
Attributes: Attributes:
active (bool): flag show if anything is being played active (bool): flag show if anything is being played
entries (list): list of all tracks entries (list): list of all tracks
json_file (str): path to save JSON playlist
mode (str): the mode to pick and play tracks mode (str): the mode to pick and play tracks
mp (MPV): an mpv instance mp (MPV): an mpv instance
play_backward (bool): flag show if to play the previous track play_backward (bool): flag show if to play the previous track
@ -68,90 +77,82 @@ class Comp(object):
playing (int): index of playing track in played playing (int): index of playing track in played
playlist (iterator): iterator of tracks according to mode playlist (iterator): iterator of tracks according to mode
reading (bool): flag show if user input is being read reading (bool): flag show if user input is being read
start (int): index of the first track to be printed on screen search_res (iterator): title-searched results
scr (curses WindowObject): curses window object scr (curses WindowObject): curses window object
start (int): index of the first track to be printed on screen
vid (str): flag show if video output is enabled vid (str): flag show if video output is enabled
y (int): the current y-coordinate y (int): the current y-coordinate
""" """
def __new__(cls, entries, mode, mpv_vo, mpv_vid, ytdlf): def __new__(cls, entries, json_file, mode, mpv_vo, mpv_vid, ytdlf):
self = super(Comp, cls).__new__(cls) self = super(Comp, cls).__new__(cls)
self.mode, self.vid = mode, mpv_vid
self.active, self.play_backward, self.reading = False, False, False self.active, self.play_backward, self.reading = False, False, False
self.playing, self.start, self.y = -1, 0, 1 self.playing, self.start, self.y = -1, 0, 1
self.entries, self.playlist, self.played = entries, iter(()), [] self.json_file, self.mode, self.vid = json_file, mode, mpv_vid
self.entries, self.played = entries, []
self.playlist, self.search_res = iter(()), deque()
self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True, self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
log_handler=mpv_logger, ytdl=True, ytdl_format=ytdlf) log_handler=mpv_logger, ytdl=True, ytdl_format=ytdlf)
self.scr = curses.initscr() self.scr = curses.initscr()
return self return self
def update_status(self, message='', msgattr=curses.A_NORMAL):
"""Update the status lines at the bottom of the screen."""
def adds(s, a, y=curses.LINES-2, x=0):
if not self.reading: self.scr.addstr(y, x, s, a)
right = ' {} {}{} '.format(_(self.mode), ' ' if self.mp.mute else 'A',
' ' if self.vid == 'no' else 'V')
adds(right.rjust(curses.COLS), curses.color_pair(12))
try:
timestr = '%M:%S' if int(self.mp.duration) < 3600 else '%H:%M:%S'
left = ' {} / {} {} '.format(
strftime(timestr, gmtime(int(self.mp.time_pos))),
strftime(timestr, gmtime(int(self.mp.duration))),
'|' if self.mp.pause else '>')
title_len = curses.COLS - len(left + right)
center = justified(self.mp.media_title, title_len)
except:
pass
else:
adds(left, curses.color_pair(12))
adds(center, curses.color_pair(12) | curses.A_BOLD, x=len(left))
if message:
adds(justified(message, curses.COLS - 1), msgattr,
y=curses.LINES-1, x=0)
self.scr.refresh()
def getlink(self, entry):
"""Return an URL from the given entry."""
if 'webpage_url' not in entry:
with YoutubeDL({'quiet': True}) as ytdl:
entry.update(ytdl.extract_info(entry['url'], download=False,
ie_key=entry.get('ie_key')))
self.uniform(entry)
return entry['webpage_url']
def setno(self, *keys): def setno(self, *keys):
"""Set all keys of each entry in entries to False.""" """Set all keys of each entry in entries to False."""
for entry in self.entries: for entry in self.entries:
for key in keys: for key in keys:
entry[key] = False entry[key] = False
def update_status(self, message='', msgattr=curses.A_NORMAL):
"""Update the status lines at the bottom of the screen."""
def adds(s, a, y=curses.LINES-2, x=0):
if not self.reading: self.scr.addstr(y, x, s, a)
def sectoosd(pos, duration):
"""Quick hack to convert a pair of seconds to HHMMSS/HHMMSS
string as MPV.get_property_osd_string isn't available.
"""
postime, durationtime = gmtime(pos), gmtime(duration)
# Let's hope media durations are shorter than a day
timestr = '%M:%S' if duration < 3600 else '%H:%M:%S'
return '{} / {}'.format(strftime(timestr, postime),
strftime(timestr, durationtime))
right = ' {} {}{} '.format(
_(self.mode), ' ' if self.mp._get_property('mute', bool) else 'A',
' ' if self.vid == 'no' else 'V')
adds(right.rjust(curses.COLS), curses.color_pair(12))
try:
left = ' {} {} '.format(
sectoosd(self.mp._get_property('time-pos', int),
self.mp._get_property('duration', int)),
'|' if self.mp._get_property('pause', bool) else '>')
title_len = curses.COLS - len(left + right)
center = self.mp._get_property('media-title').ljust(title_len)[:title_len]
except:
pass
else:
adds(left, curses.color_pair(12))
adds(center, curses.color_pair(12) | curses.A_BOLD, x=len(left))
if message:
self.scr.move(curses.LINES - 1, 0)
self.scr.clrtoeol()
adds(message.ljust(curses.COLS)[:curses.COLS-1], msgattr,
y=curses.LINES-1, x=0)
self.scr.refresh()
def getlink(self, entry):
"""Return an URL from the given entry."""
with YoutubeDL({'quiet': True}) as ytdl:
return entry.setdefault('webpage_url', ytdl.extract_info(
entry['url'], download=False, ie_key=entry.get('ie_key')
).get('webpage_url'))
def play(self, force=False): def play(self, force=False):
"""Play the next track.""" """Play the next track."""
def mpv_play(entry, force): def mpv_play(entry, force):
self.active = True self.active = True
self.setno('playing') self.setno('playing')
entry['playing'] = True entry['playing'] = True
self.redraw() self.mp.vid = self.vid
self.mp._set_property('vid', self.vid)
try: try:
self.mp.play(self.getlink(entry)) self.mp.play(self.getlink(entry))
except: except:
entry['error'] = True entry['error'] = True
if force: self.mp._set_property('pause', False, bool) self.print(entry)
if force: self.mp.pause = False
self.mp.wait_for_playback() self.mp.wait_for_playback()
self.active = False self.active = False
entry['playing'] = False entry['playing'] = False
self.redraw() self.print(entry)
if self.play_backward and -self.playing < len(self.played): if self.play_backward and -self.playing < len(self.played):
self.playing -= 1 self.playing -= 1
@ -171,48 +172,66 @@ class Comp(object):
play_thread = Thread(target=mpv_play, args=t, daemon=True) play_thread = Thread(target=mpv_play, args=t, daemon=True)
play_thread.start() play_thread.start()
def generic_event_handler(self, event): def uniform(self, entry):
"""Reprint status line and play next entry if the last one is """Standardize data format."""
ended without caring about the event. for i in 'error', 'playing', 'selected': entry.setdefault(i, False)
""" entry.setdefault('ie_key', entry.get('extractor'))
self.update_status() entry.setdefault('duration', 0)
if not self.active: self.play() if 'title' not in entry: # or 'webpage_url' not in entry:
with YoutubeDL({'quiet': True}) as ytdl:
entry.update(ytdl.extract_info(entry['url'], download=False,
ie_key=entry.get('ie_key')))
for i in entry.copy():
if i not in ('duration', 'error', 'playing', 'selected',
'ie_key', 'title', 'url', 'webpage_url'):
entry.pop(i)
def reattr(self, y=None): def _writeln(self, y, title, duration, attr):
"""Set the attributes of line y, if y is None the current line title_len = curses.COLS-DURATION_COL_LEN-3
will be picked.""" title = justified(title, title_len)
if y is None: y = self.y duration = duration.ljust(DURATION_COL_LEN)
entry = self.entries[self.start + y - 1] self.scr.addstr(y, 0, ' {} {} '.format(title, duration), attr)
self.scr.refresh()
def print(self, entry=None, y=None):
"""Print the entry in the line y."""
if entry is y is None:
entry = self.current()
y = self.idx() - self.start + 1
elif entry is None:
entry = self.entries[self.start + y - 1]
elif y is None:
y = self.idx(entry) - self.start + 1
if y < 1 or y > curses.LINES - 3: return
self.uniform(entry)
c = {'error': 1, 'playing': 3, 'selected': 5} c = {'error': 1, 'playing': 3, 'selected': 5}
color = ((8 if entry is self.current() else 0) color = ((8 if entry is self.current() else 0)
| reduce(int.__xor__, (c.get(i, 0) for i in entry if entry[i]))) | reduce(int.__xor__, (c.get(i, 0) for i in entry if entry[i])))
duration = strftime('%H:%M:%S', gmtime(entry['duration']))
if color: if color:
self.scr.chgat(y, 0, curses.color_pair(color) | curses.A_BOLD) self._writeln(y, entry['title'], duration,
curses.color_pair(color) | curses.A_BOLD)
else: else:
self.scr.chgat(y, 0, curses.A_NORMAL) self._writeln(y, entry['title'], duration,
curses.A_NORMAL)
def redraw(self): def redraw(self):
"""Redraw the whole screen.""" """Redraw the whole screen."""
def _max(a): return max(a) if a else 0 self._writeln(0, _("Title"), _("Duration"),
self.scr.clear() curses.color_pair(10) | curses.A_BOLD)
self.scr.addstr(0, 1, _('Title'))
sitenamelen = max(_max([len(entry['ie_key']) for entry in self.entries]), 6)
self.scr.addstr(0, curses.COLS - sitenamelen - 1, _('Source'))
self.scr.chgat(0, 0, curses.color_pair(10) | curses.A_BOLD)
for i, entry in enumerate(self.entries[self.start:][:curses.LINES-3]): for i, entry in enumerate(self.entries[self.start:][:curses.LINES-3]):
self.scr.addstr(i + 1, 0, entry['ie_key'].rjust(curses.COLS - 1)) self.print(entry, i + 1)
self.scr.addstr(i + 1, 1, entry.get('title', '')[:curses.COLS-sitenamelen-3]) self.scr.clrtobot()
self.reattr(i + 1)
self.update_status() self.update_status()
def __init__(self, entries, mode, mpv_vo, mpv_vid, ytdlf): def __init__(self, json_file, entries, mode, mpv_vo, mpv_vid, ytdlf):
self.setno('error', 'playing', 'selected') if mpv_vo is not None: self.mp['vo'] = mpv_vo
self.mp.observe_property('mute', lambda x: self.update_status())
if mpv_vo: self.mp['vo'] = mpv_vo self.mp.observe_property('pause', lambda x: self.update_status())
self.mp.observe_property('mute', lambda event: self.update_status()) self.mp.observe_property('time-pos', lambda x: self.update_status())
self.mp.observe_property('pause', lambda event: self.update_status()) self.mp.observe_property('duration', lambda x: None if self.active
self.mp.observe_property('time-pos', self.generic_event_handler) else self.play())
curses.noecho() curses.noecho()
curses.cbreak() curses.cbreak()
self.scr.keypad(True) self.scr.keypad(True)
@ -226,9 +245,11 @@ class Comp(object):
def __enter__(self): return self def __enter__(self): return self
def idx(self): def idx(self, entry=None):
"""Return the index of the current entry.""" """Return the index of the current entry."""
return self.start + self.y - 1 if entry is None:
return self.start + self.y - 1
return self.entries.index(entry)
def current(self): def current(self):
"""Return the current entry.""" """Return the current entry."""
@ -242,9 +263,10 @@ class Comp(object):
if pick == 'current': if pick == 'current':
self.play_list = [self.current()] self.play_list = [self.current()]
elif pick == 'all': elif pick == 'all':
self.play_list = self.entries self.play_list = deque(self.entries)
self.play_list.rotate(-self.idx())
else: else:
self.play_list = [i for i in self.entries if i['selected']] self.play_list = [i for i in self.entries if i.get('selected')]
def update_playlist(self): def update_playlist(self):
"""Update the playlist to be used by play function.""" """Update the playlist to be used by play function."""
@ -256,12 +278,48 @@ class Comp(object):
self.playlist = cycle(self.play_list) self.playlist = cycle(self.play_list)
else: else:
self.playlist = iter(lambda: choice(self.play_list), None) self.playlist = iter(lambda: choice(self.play_list), None)
self.played = self.played[:self.playing] if self.playing < -1: self.played = self.played[:self.playing+1]
def gets(self, prompt):
"""Print the prompt string at the bottom of the screen then read
from standard input.
"""
self.scr.addstr(curses.LINES - 1, 0,
justified(prompt, curses.COLS - 1))
self.reading = True
curses.curs_set(True)
curses.echo()
b = self.scr.getstr(curses.LINES - 1, len(prompt))
self.reading = False
curses.curs_set(False)
curses.noecho()
return b.decode()
def seek(self, amount, reference='relative', precision='default-precise'):
"""Wrap mp.seek with a try clause to avoid crash when nothing is
being played.
"""
try:
self.mp.seek(amount, reference, precision)
except:
pass
def next(self, force=False, backward=False):
comp.play_backward = backward
if self.active:
self.seek(100, 'absolute-percent')
if force: self.mp.pause = False
else:
self.play(force)
def download(self):
with YoutubeDL({'quiet': True}) as ytdl:
ytdl.download([self.getlink(i) for i in self.play_list])
def move(self, delta): def move(self, delta):
"""Move to the relatively next delta entry.""" """Move to the relatively next delta entry."""
if not (self.entries and delta): return if not (self.entries and delta): return
start, y = self.start, self.y start, prev_entry = self.start, self.current()
maxy = min(len(self.entries), curses.LINES - 3) maxy = min(len(self.entries), curses.LINES - 3)
if self.idx() + delta <= 0: if self.idx() + delta <= 0:
@ -278,104 +336,158 @@ class Comp(object):
self.y += delta self.y += delta
if self.start == start: if self.start == start:
self.reattr(y) self.print(prev_entry)
self.reattr() self.print()
self.scr.refresh()
else: else:
self.redraw() self.redraw()
def gets(self, prompt): def search(self, backward=False):
"""Print the prompt string at the bottom of the screen then read """Prompt then search for a pattern."""
from standard input. p = re.compile(self.gets('/'), re.IGNORECASE)
""" entries = deque(self.entries)
self.scr.addstr(curses.LINES - 1, 0, prompt) entries.rotate(-self.idx())
self.reading = True self.search_res = deque(filter(
curses.curs_set(True) lambda entry: p.search(entry['title']) is not None, entries))
curses.echo() if backward: self.search_res.reverse()
b = self.scr.getstr(curses.LINES - 1, len(prompt)) if self.search_res:
self.reading = False self.move(self.idx(self.search_res[0]) - self.idx())
curses.curs_set(False) else:
curses.noecho() self.update_status(_("Pattern not found"), curses.color_pair(1))
return b.decode()
def seek(self, amount, reference='relative'): def next_search(self, backward=False):
"""Quick hack to fix MPV seek-double bug.""" """Repeat previous search."""
if reference == 'relative': amount /= 2 if self.search_res:
try: self.search_res.rotate(1 if backward else -1)
self.mp.seek(amount, reference) self.move(self.idx(self.search_res[0]) - self.idx())
except: else:
pass self.update_status(_("Pattern not found"), curses.color_pair(1))
def resize(self):
curses.update_lines_cols()
l = curses.LINES - 3
if curses.COLS < MODE_STR_LEN + 42 or l < 1:
sizeerr = _("Current size: {}x{}. Minimum size: {}x4.").format(
curses.COLS, curses.LINES, MODE_STR_LEN + 42)
self.scr.addstr(0, 0, sizeerr[:curses.LINES*curses.COLS-1])
elif self.y > l:
self.start += self.y - l
self.y = l
self.redraw()
elif 0 < self.start > len(self.entries) - l:
idx, self.start = self.idx(), min(0, len(entries) - l)
self.y = idx - self.start + 1
if self.y > l:
self.start += self.y - l
self.y = l
self.redraw()
else:
self.redraw()
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
curses.nocbreak() curses.nocbreak()
self.scr.keypad(False) self.scr.keypad(False)
curses.echo() curses.echo()
curses.endwin() curses.endwin()
self.mp.terminate() self.mp.quit()
parser = ArgumentParser(description=_("Curses Online Media Player")) parser = ArgumentParser(description=_("Curses Online Media Player"))
parser.add_argument('-c', '--config', required=False, parser.add_argument('-c', '--config', required=False,
help=_('path to custom config file')) help=_("location of the configuration file; either the\
path to the config or its containing directory"))
parser.add_argument('--vid', choices=('ID', 'auto', 'no'), required=False,
help=_("initial video channel. auto selects the default,\
no disables video"))
parser.add_argument('--vo', required=False, metavar='DRIVER',
help=_("specify the video output backend to be used. See\
VIDEO OUTPUT DRIVERS in mpv(1) man page for\
details and descriptions of available drivers"))
parser.add_argument('-f', '--format', required=False, metavar='YTDL_FORMAT',
help=_("video format/quality to be passed to youtube-dl"))
parser.add_argument('-u', '--online-playlist', required=False, metavar='URL', parser.add_argument('-u', '--online-playlist', required=False, metavar='URL',
help=_('URL to an playlist on Youtube')) help=_("URL to an playlist on Youtube"))
parser.add_argument('-j', '--json-playlist', required=False, metavar='path', parser.add_argument('-j', '--json', required=False, metavar='JSON_PLAYLIST',
help=_('path to playlist in JSON format')) help=_("path to playlist in JSON format. If\
--online-playlist is already specified, this will\
be used as the default file to save the playlist"))
args = parser.parse_args() args = parser.parse_args()
config = ConfigParser() config = ConfigParser()
config.read(USER_CONFIG if isfile(USER_CONFIG) else SYSTEM_CONFIG) if args.config is not None:
if isfile(args.config):
config_file = args.config
else: #isdir(args.config_location):
config_file = join(args.config, 'settings.ini')
elif isfile(USER_CONFIG):
config_file = USER_CONFIG
else:
config_file = SYSTEM_CONFIG
config.read(config_file)
if args.json_playlist: if args.json is not None:
json_file = args.json_playlist json_file = args.json
with open(json_file) as f: else:
entries = json.load(f) json_file = ''
elif args.online_playlist:
if args.online_playlist is not None:
with YoutubeDL({'extract_flat': 'in_playlist', 'quiet': True}) as ytdl: with YoutubeDL({'extract_flat': 'in_playlist', 'quiet': True}) as ytdl:
info = ytdl.extract_info(args.online_playlist, download=False) info = ytdl.extract_info(args.online_playlist, download=False)
entries = info.get('entries', []) entries = info.get('entries', [info])
json_file = ''
else: else:
entries = [] try:
json_file = '' with open(json_file) as f: entries = json.load(f)
except FileNotFoundError:
entries = []
if args.vid is not None:
vid = args.vid
else:
vid = config.get('mpv', 'video', fallback='auto')
if args.vo is not None:
vo = args.vo
else:
vo = config.get('mpv', 'video-output', fallback=None)
mode = config.get('comp', 'play-mode', fallback='play-current')
if args.format is not None:
ytdlf = args.format
else:
ytdlf = config.get('youtube-dl', 'format', fallback='best')
makedirs(dirname(MPV_LOG), exist_ok=True) makedirs(dirname(MPV_LOG), exist_ok=True)
with Comp( with Comp(entries, json_file, mode, vo, vid, ytdlf) as comp:
entries,
config.get('comp', 'play-mode', fallback='play-current'),
config.get('mpv', 'video-output', fallback=None),
config.get('mpv', 'video', fallback='auto'),
config.get('youtube-dl', 'format', fallback='best')
) as comp:
c = comp.scr.getch() c = comp.scr.getch()
while c != 113: # letter q while c != 113: # letter q
if c == 10: # curses.KEY_ENTER doesn't work if c == 10: # curses.KEY_ENTER doesn't work
comp.update_playlist() comp.update_playlist()
if comp.active: comp.next(force=True)
comp.seek(100, 'absolute-percent')
comp.mp._set_property('pause', False, bool)
else:
comp.play(force=True)
elif c == 32: # space elif c == 32: # space
comp.current()['selected'] = not comp.current()['selected'] comp.current()['selected'] = not comp.current().get('selected')
comp.move(1) comp.move(1)
elif c == 47: # /
comp.search()
elif c == 60: # < elif c == 60: # <
try: try:
if comp.mp._get_property('time-pos', int) < 5: if comp.mp._get_property('time-pos', float) < 1:
comp.play_backward = True comp.next(backward=True)
comp.seek(100, 'absolute-percent')
else: else:
comp.seek(0, 'absolute') comp.seek(0, 'absolute')
except: except:
pass pass
elif c == 62: # > elif c == 62: # >
comp.seek(100, 'absolute-percent') comp.next()
elif c == 63: # ?
comp.search(backward=True)
elif c == 65: # letter A elif c == 65: # letter A
comp.mp._toggle_property('mute') comp.mp._toggle_property('mute')
elif c == 77: # letter M elif c == 77: # letter M
comp.mode = MODES[(MODES.index(comp.mode) - 1) % 8] comp.mode = MODES[(MODES.index(comp.mode) - 1) % 8]
comp.update_status() comp.update_status()
elif c == 78: # letter N
comp.next_search(backward=True)
elif c == 85: # letter U elif c == 85: # letter U
with YoutubeDL({'extract_flat': True, 'quiet': True}) as ytdl: with YoutubeDL({'extract_flat': True, 'quiet': True}) as ytdl:
try: try:
@ -384,30 +496,26 @@ with Comp(
except: except:
comp.redraw() comp.redraw()
else: else:
comp.entries = info.get('entries', {}) comp.entries = info.get('entries', [info])
comp.start, comp.y = 0, 1 comp.start, comp.y = 0, 1
comp.setno('error', 'playing', 'selected')
comp.redraw() comp.redraw()
elif c == 86: # letter V elif c == 86: # letter V
comp.vid = 'auto' if comp.vid == 'no' else 'no' comp.vid = 'auto' if comp.vid == 'no' else 'no'
comp.mp._set_property('vid', comp.vid) comp.mp.vid = comp.vid
comp.update_status() comp.update_status()
elif c == 87: # letter W elif c == 87: # letter W
if not comp.entries: continue s = comp.gets(_("Save playlist to [{}]: ").format(comp.json_file))
s = comp.gets(_('Save playlist to [{}]: ').format(json_file)) if s: comp.json_file = s
if s: json_file = s
try: try:
makedirs(dirname(abspath(json_file)), exist_ok=True) makedirs(dirname(abspath(comp.json_file)), exist_ok=True)
with open(json_file, 'w') as f: with open(comp.json_file, 'w') as f: json.dump(comp.entries, f)
json.dump(entries, f)
except: except:
comp.update_status( errmsg = _("'{}': Can't open file for writing").format(
_("'{}': Can't open file for writing").format(json_file), comp.json_file)
curses.color_pair(1)) comp.update_status(errmsg, curses.color_pair(1))
else: else:
comp.update_status(_("'{}' written").format(json_file)) comp.update_status(_("'{}' written").format(comp.json_file))
elif c == 100: # letter d elif c == 100: # letter d
if not comp.entries: continue
i = comp.idx() i = comp.idx()
if i + 1 < len(entries): if i + 1 < len(entries):
comp.entries.pop(i) comp.entries.pop(i)
@ -421,18 +529,20 @@ with Comp(
elif c == 109: # letter m elif c == 109: # letter m
comp.mode = MODES[(MODES.index(comp.mode) + 1) % 8] comp.mode = MODES[(MODES.index(comp.mode) + 1) % 8]
comp.update_status() comp.update_status()
elif c == 110: # letter n
comp.next_search()
elif c == 119: # letter w elif c == 119: # letter w
comp.update_play_list(comp.mode.split('-')[1]) comp.update_play_list(comp.mode.split('-')[1])
with YoutubeDL({'quiet': True}) as ytdl: play_thread = Thread(target=comp.download, daemon=True)
ytdl.download([comp.getlink(i) for i in comp.play_list]) play_thread.start()
elif c in (curses.KEY_UP, 107): # up arrow or letter k elif c in (curses.KEY_UP, 107): # up arrow or letter k
comp.move(-1) comp.move(-1)
elif c in (curses.KEY_DOWN, 106): # down arrow or letter j elif c in (curses.KEY_DOWN, 106): # down arrow or letter j
comp.move(1) comp.move(1)
elif c in (curses.KEY_LEFT, 104): # left arrow or letter h elif c in (curses.KEY_LEFT, 104): # left arrow or letter h
comp.seek(-5) comp.seek(-5, precision='exact')
elif c in (curses.KEY_RIGHT, 108): # right arrow or letter l elif c in (curses.KEY_RIGHT, 108): # right arrow or letter l
comp.seek(5) comp.seek(5, precision='exact')
elif c == curses.KEY_HOME: # home elif c == curses.KEY_HOME: # home
comp.move(-len(comp.entries)) comp.move(-len(comp.entries))
elif c == curses.KEY_END: # end elif c == curses.KEY_END: # end
@ -441,17 +551,6 @@ with Comp(
comp.move(curses.LINES - 4) comp.move(curses.LINES - 4)
elif c == curses.KEY_PPAGE: # page up elif c == curses.KEY_PPAGE: # page up
comp.move(4 - curses.LINES) comp.move(4 - curses.LINES)
elif c == curses.KEY_F5: # F5 elif c in (curses.KEY_F5, curses.KEY_RESIZE):
comp.redraw() comp.resize()
elif c == curses.KEY_RESIZE:
curses.update_lines_cols()
if curses.COLS < MODE_STR_LEN + 42 or curses.LINES < 4:
comp.scr.clear()
sizeerr = _('Current size: {}x{}. Minimum size: {}x4.').format(
curses.COLS, curses.LINES, MODE_STR_LEN + 42)
comp.scr.addstr(0, 0, sizeerr[:curses.LINES*curses.COLS])
else:
comp.start += comp.y - 1
comp.y = 1
comp.redraw()
c = comp.scr.getch() c = comp.scr.getch()

View file

@ -1,15 +1,17 @@
[comp] [comp]
# Supported 8 modes: play-current, play-all, play-selected, repeat-current, # Initial playing mode, which can be one of these 8 modes: play-current,
# repeat-all, repeat-selected, shuffle-all and shuffle-selected. # play-all, play-selected, repeat-current, repeat-all, repeat-selected,
# shuffle-all and shuffle-selected.
play-mode = play-current play-mode = play-current
[mpv] [mpv]
# Set if video should be download and play, I only know 2 possible values: # Initial video channel. auto selects the default, no disables video.
# auto and no. This can be changed later interactively.
video = auto video = auto
# Read more on VIDEO OUTPUT DRIVERS section in mpv man page. # Specify the video output backend to be used. See VIDEO OUTPUT DRIVERS in
# mpv(1) man page for details and descriptions of available drivers.
video-output = video-output =
[youtube-dl] [youtube-dl]
# Read more on FORMAT SELECTION section in youtube-dl man page. # Video format/quality to be passed to youtube-dl. See FORMAT SELECTION in
# youtube-dl(1) man page for more details and descriptions.
format = best format = best

View file

@ -8,7 +8,7 @@ from sys import prefix
with open('README.rst') as f: with open('README.rst') as f:
long_description = f.read() long_description = f.read()
setup(name='comp', version='0.2.0a1', setup(name='comp', version='0.2.1',
url='https://github.com/McSinyx/comp', url='https://github.com/McSinyx/comp',
description=('Curses Online Media Player'), description=('Curses Online Media Player'),
long_description=long_description, long_description=long_description,
@ -19,7 +19,7 @@ setup(name='comp', version='0.2.0a1',
for i in walk('locale') if i[2]), for i in walk('locale') if i[2]),
('/etc/comp', ['settings.ini']) ('/etc/comp', ['settings.ini'])
], classifiers=[ ], classifiers=[
'Development Status :: 3 - Alpha', 'Development Status :: 4 - Beta',
'Environment :: Console :: Curses', 'Environment :: Console :: Curses',
'Intended Audience :: End Users/Desktop', 'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU Affero General Public License v3', 'License :: OSI Approved :: GNU Affero General Public License v3',