Update documentations and clean up (?)

This commit is contained in:
Nguyễn Gia Phong 2017-06-25 17:38:19 +07:00 committed by Nguyễn Gia Phong
parent 801c439146
commit 6d0aa7fe51
5 changed files with 309 additions and 133 deletions

View File

@ -2,17 +2,21 @@
comp - Curses Omni Media Player
===============================
This program is a curses front-end for mpv and youtube-dl.
comp is a mpv front-end using curses. It has basic media player functions and
can to extract playlists from multiple sources such as media sites supported by
youtube-dl, local and direct URL to video/audio and its own JSON playlist
format.
.. image:: doc/screenshot.png
.. image:: https://github.com/McSinyx/comp/raw/master/doc/screenshot.png
Installation
------------
comp requires Python 3.5+ with ``curses`` module (only available on Unix-like
OSes such as GNU/Linux and the BSDs) and ``libmpv``. It also depends on
``python-mpv`` and ``youtube-dl`` but the setup program will automatically
install them if they are missing.
OSes such as GNU/Linux and the BSDs) and ``libmpv`` (available as ``libmpv1``
in Debian/Ubuntu, openSUSE; and as ``mpv`` in Arch Linux, Gentoo, macOS
Homebrew repository). It also depends on ``python-mpv`` and ``youtube-dl`` but
the setup program will automatically install them if they are missing.
Using pip
^^^^^^^^^
@ -24,35 +28,31 @@ installation.
Using setup.py
^^^^^^^^^^^^^^
To install the latest version or test the development branch (called
``bachelor``, in contrast to ``master``), you'll need to do it manually::
To install the latest version or test development branches, you'll need to do
it manually::
git clone https://github.com/McSinyx/comp.git
cd comp
git checkout bachelor # usually master is synced with the PyPI repo
./setup.py install -e .
./setup.py install
Note ``setup.py`` uses ``setuptools`` which is a third-party module and can be
install using ``pip3``.
Note that ``setup.py`` uses ``setuptools`` which is a third-party module and
can be install using ``pip3``.
Usage
-----
Command line arguments
^^^^^^^^^^^^^^^^^^^^^^
Command line options
--------------------
::
$ comp --help
usage: comp [-h] [-e {json,mpv,youtube-dl}] [-c CONFIG] [--vid VID]
[--vo DRIVER] [-f YTDL_FORMAT]
file
Curses Online Media Player
playlist
Curses Omni Media Player
positional arguments:
file path or URL to the playlist to be opened
playlist path or URL to the playlist
optional arguments:
-h, --help show this help message and exit
-e {json,mpv,youtube-dl}, --extractor {json,mpv,youtube-dl}
@ -61,66 +61,75 @@ Command line arguments
path to the configuration file
--vid VID 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
--vo DRIVER specify the video output backend to be used. See
VIDEO OUTPUT DRIVERS in mpv(1) for details and
descriptions of available drivers
-f YTDL_FORMAT, --format YTDL_FORMAT
video format/quality to be passed to youtube-dl
Keyboard control
^^^^^^^^^^^^^^^^
Examples
^^^^^^^^
+--------------+---------------------------------------------+
| Key | Action |
+==============+=============================================+
| Return | Start playing |
+--------------+---------------------------------------------+
| Space | Select the current track |
+--------------+---------------------------------------------+
| ``/``, ``?`` | Search forward/backward for a pattern |
+--------------+---------------------------------------------+
| ``<``, ``>`` | Go forward/backward in the playlist |
+--------------+---------------------------------------------+
| ``A`` | Toggle mute |
+--------------+---------------------------------------------+
| ``N`` | Repeat previous search in reverse direction |
+--------------+---------------------------------------------+
| ``V`` | Toggle video |
+--------------+---------------------------------------------+
| ``W`` | Save the current playlist under JSON format |
+--------------+---------------------------------------------+
| ``d`` | Delete current entry |
+--------------+---------------------------------------------+
| ``i`` | Insert playlist |
+--------------+---------------------------------------------+
| ``m``, ``M`` | Cycle through playing modes |
+--------------+---------------------------------------------+
| ``n`` | Repeat previous search |
+--------------+---------------------------------------------+
| ``p`` | Toggle pause |
+--------------+---------------------------------------------+
| ``o`` | Open playlist |
+--------------+---------------------------------------------+
| ``w`` | Download tracks set by playing mode |
+--------------+---------------------------------------------+
| Up, ``k`` | Move a single line up |
+--------------+---------------------------------------------+
| Down, ``j`` | Move a single line down |
+--------------+---------------------------------------------+
| Left, ``h`` | Seek backward 5 seconds |
+--------------+---------------------------------------------+
| Right, ``l`` | Seek forward 5 seconds |
+--------------+---------------------------------------------+
| Home | Move to the beginning of the playlist |
+--------------+---------------------------------------------+
| End | Move to the end of the playlist |
+--------------+---------------------------------------------+
| Page Up | Move a single page up |
+--------------+---------------------------------------------+
| Page Down | Move a single page down |
+--------------+---------------------------------------------+
| F5 | Redraw the screen content |
+--------------+---------------------------------------------+
Open a JSON playlist::
comp -e json test/playlist.json
Open a Youtube playlist with video height lower than 720::
comp -f [height<720] https://www.youtube.com/watch?v=pqkHrdYXaTk&list=PLnk14Iku8QM7R3ARnrj1TwYSZleF-i7jT
Keyboard control
----------------
+--------------+----------------------------------------------+
| Key | Action |
+==============+==============================================+
| Return | Start playing |
+--------------+----------------------------------------------+
| Space | Select the current track |
+--------------+----------------------------------------------+
| ``/``, ``?`` | Search forward/backward for a pattern |
+--------------+----------------------------------------------+
| ``<``, ``>`` | Go backward/forward in the playlist |
+--------------+----------------------------------------------+
| ``A`` | Toggle mute |
+--------------+----------------------------------------------+
| ``N`` | Repeat previous search in reverse direction |
+--------------+----------------------------------------------+
| ``V`` | Toggle video |
+--------------+----------------------------------------------+
| ``W`` | Save the current playlist under JSON format |
+--------------+----------------------------------------------+
| ``d`` | Delete current entry |
+--------------+----------------------------------------------+
| ``i`` | Insert playlist |
+--------------+----------------------------------------------+
| ``m``, ``M`` | Cycle forward/backward through playing modes |
+--------------+----------------------------------------------+
| ``n`` | Repeat previous search |
+--------------+----------------------------------------------+
| ``p`` | Toggle pause |
+--------------+----------------------------------------------+
| ``o`` | Open playlist |
+--------------+----------------------------------------------+
| Up, ``k`` | Move a single line up |
+--------------+----------------------------------------------+
| Down, ``j`` | Move a single line down |
+--------------+----------------------------------------------+
| Left, ``h`` | Seek backward 5 seconds |
+--------------+----------------------------------------------+
| Right, ``l`` | Seek forward 5 seconds |
+--------------+----------------------------------------------+
| Home | Move to the beginning of the playlist |
+--------------+----------------------------------------------+
| End | Move to the end of the playlist |
+--------------+----------------------------------------------+
| Page Up | Move a single page up |
+--------------+----------------------------------------------+
| Page Down | Move a single page down |
+--------------+----------------------------------------------+
| F5 | Redraw the screen content |
+--------------+----------------------------------------------+
Configuration files
-------------------
@ -146,3 +155,12 @@ are listed below::
# 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
Bugs
----
Media durations are not extracted from online playlists as
``youtube-dl.YoutubeDL`` option ``extract_flat`` is set to ``'in_playlist'``.
This is rather a feature to save up bandwidth than a bug because a track's
duration is updated when it's played.

75
comp
View File

@ -25,7 +25,7 @@ from configparser import ConfigParser
from functools import reduce
from gettext import bindtextdomain, gettext as _, textdomain
from os import makedirs
from os.path import abspath, dirname, expanduser
from os.path import abspath, dirname, expanduser, expandvars
from threading import Thread
from mpv import MPV
@ -111,7 +111,11 @@ class Comp(Omp):
add_status_str(_(self.mode), x=-5-len(_(self.mode)))
if not self.mp.mute: add_status_str('A', x=-4, X=-3)
if self.vid != 'no': add_status_str('V', x=-2, lpad=0)
if message: self.adds(message, curses.LINES-1, attr=msgattr, lpad=0)
self.scr.refresh()
def print_msg(self, message, error=False):
attributes = curses.color_pair(1) if error else curses.A_NORMAL
self.adds(message, curses.LINES-1, attr=attributes, lpad=0)
self.scr.refresh()
def setno(self, *keys):
@ -182,7 +186,7 @@ class Comp(Omp):
self._writeln(y, entry['title'], entry['duration'],
curses.A_NORMAL)
def redraw(self):
def refresh(self):
"""Redraw the whole screen."""
self._writeln(0, _("Title"), _("Duration"),
curses.color_pair(10) | curses.A_BOLD)
@ -191,9 +195,10 @@ class Comp(Omp):
self.scr.clrtobot()
self.update_status()
def property_handler(self, name, val): self.update_status()
def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
Omp.__init__(self, entries, lambda name, val: self.update_status(),
json_file, mode, mpv_vid, mpv_vo, ytdlf)
Omp.__init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf)
curses.noecho()
curses.cbreak()
self.scr.keypad(True)
@ -203,7 +208,7 @@ class Comp(Omp):
for i in range(1, 8): curses.init_pair(i, i, -1)
curses.init_pair(8, -1, 7)
for i in range(1, 7): curses.init_pair(i + 8, -1, i)
self.redraw()
self.refresh()
def __enter__(self): return self
@ -220,7 +225,7 @@ class Comp(Omp):
except:
return {}
def gets(self, prompt):
def read_input(self, prompt):
"""Print the prompt string at the bottom of the screen then read
from standard input.
"""
@ -266,11 +271,11 @@ class Comp(Omp):
self.print(prev_entry)
self.print()
else:
self.redraw()
self.refresh()
def search(self, backward=False):
"""Prompt then search for a pattern."""
p = re.compile(self.gets('/'), re.IGNORECASE)
p = re.compile(self.read_input('/'), re.IGNORECASE)
entries = deque(self.entries)
entries.rotate(-self.idx())
self.search_res = deque(filter(
@ -279,7 +284,7 @@ class Comp(Omp):
if self.search_res:
self.move(self.idx(self.search_res[0]) - self.idx())
else:
self.update_status(_("Pattern not found"), curses.color_pair(1))
self.print_msg(_("Pattern not found"), error=True)
def next_search(self, backward=False):
"""Repeat previous search."""
@ -287,7 +292,7 @@ class Comp(Omp):
self.search_res.rotate(1 if backward else -1)
self.move(self.idx(self.search_res[0]) - self.idx())
else:
self.update_status(_("Pattern not found"), curses.color_pair(1))
self.print_msg(_("Pattern not found"), error=True)
def resize(self):
curses.update_lines_cols()
@ -301,16 +306,16 @@ class Comp(Omp):
elif self.y > l: # shorter than the current entry
self.start += self.y - l
self.y = l
self.redraw()
self.refresh()
elif 0 < self.start > len(self.entries) - l: # longer than the list
idx, self.start = self.idx(), min(0, len(self.entries) - l)
self.y = idx - self.start + 1
if self.y > l:
self.start += self.y - l
self.y = l
self.redraw()
self.refresh()
else:
self.redraw()
self.refresh()
def __exit__(self, exc_type, exc_value, traceback):
curses.nocbreak()
@ -324,7 +329,7 @@ parser = ArgumentParser(description=_("Curses Omni Media Player"))
parser.add_argument('-e', '--extractor', default='youtube-dl',
choices=('json', 'mpv', 'youtube-dl'), required=False,
help=_("playlist extractor, default is youtube-dl"))
parser.add_argument('file', help=_("path or URL to the playlist to be opened"))
parser.add_argument('playlist', help=_("path or URL to the playlist"))
parser.add_argument('-c', '--config', default=USER_CONFIG, required=False,
help=_("path to the configuration file"))
parser.add_argument('--vid', required=False,
@ -332,16 +337,16 @@ parser.add_argument('--vid', required=False,
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"))
VIDEO OUTPUT DRIVERS in mpv(1) 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"))
args = parser.parse_args()
entries = extract_info(args.file, args.extractor)
entries = extract_info(args.playlist, args.extractor)
if entries is None:
print(_("'{}': Can't extract playlist").format(args.file))
exit()
json_file = args.file if args.extractor == 'json' else ''
json_file = args.playlist if args.extractor == 'json' else ''
config = ConfigParser()
config.read(args.config)
vid = args.vid or config.get('mpv', 'video', fallback='auto')
@ -384,54 +389,42 @@ with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
comp.mp.vid = comp.vid
comp.update_status()
elif c == 87: # letter W
s = comp.gets(_("Save playlist to [{}]: ").format(comp.json_file))
if s: comp.json_file = s
try:
makedirs(dirname(abspath(comp.json_file)), exist_ok=True)
with open(comp.json_file, 'w') as f:
json.dump(comp.entries, f, ensure_ascii=False,
indent=2, sort_keys=True)
except:
errmsg = _("'{}': Can't open file for writing").format(
comp.json_file)
comp.update_status(errmsg, curses.color_pair(1))
else:
comp.update_status(_("'{}' written").format(comp.json_file))
comp.dump_json()
elif c == 100: # letter d
comp.entries.pop(comp.idx())
if 1 < len(comp.entries) - curses.LINES + 4 == comp.start:
comp.start -= 1
elif comp.idx() == len(comp.entries):
comp.y -= 1
comp.redraw()
comp.refresh()
elif c == 105: # letter i
extractor = comp.gets(_("Playlist extractor: "))
filename = comp.gets(_("Insert: "))
extractor = comp.read_input(_("Playlist extractor: "))
filename = comp.read_input(_("Insert: "))
entries = extract_info(filename, extractor)
if entries is None:
comp.update_status(
comp.print_msg(
_("'{}': Can't extract playlist").format(filename))
else:
bottom = comp.entries[comp.idx():]
comp.entries = comp.entries[:comp.idx()]
comp.entries.extend(entries)
comp.entries.extend(bottom)
comp.redraw()
comp.refresh()
elif c == 109: # letter m
comp.mode = MODES[(MODES.index(comp.mode) + 1) % 8]
comp.update_status()
elif c == 110: # letter n
comp.next_search()
elif c == 111: # letter o
extractor = comp.gets(_("Playlist extractor: "))
filename = comp.gets(_("Open: "))
extractor = comp.read_input(_("Playlist extractor: "))
filename = comp.read_input(_("Open: "))
entries = extract_info(filename, extractor)
if entries is None:
comp.update_status(
comp.print_msg(
_("'{}': Can't extract playlist").format(filename))
else:
comp.entries, comp.start, comp.y = entries, 0, 1
comp.redraw()
comp.refresh()
elif c == 112: # letter p
comp.mp.pause ^= True
elif c in (curses.KEY_UP, 107): # up arrow or letter k

142
doc/comp.1 Normal file
View File

@ -0,0 +1,142 @@
.\" Process this file with
.\" groff -man -Tutf8 comp.1
.\"
.TH COMP 1 2017-06-17 comp
.SH NAME
comp \- Curses Omni Media Player
.SH SYNOPSIS
\fBcomp\fR [\fB-h\fR] [\fB-e\fR {json,mpv,youtube-dl}] [\fB-c \fICONFIG\fR]
[\fB--vid \fIVID\fR] [\fB--vo \fIDRIVER\fR] [\fB-f \fIYTDL_FORMAT\fR]
\fIplaylist\fR
.SH DESCRIPTION
\fBcomp\fR is a
.BR mpv (1)
front-end using curses. It has basic media player functions and can to extract
playlists from multiple sources such as media sites supported by
.BR youtube-dl (1),
local and direct URL to video/audio and its own JSON playlist format.
.SH OPTIONS
.SS Positional arguments
.TP
.B playlist
path or URL to the playlist
.SS Optional arguments
.TP
.B -h, --help
show this help message and exit
.TP
.B -e \fR{json,mpv,youtube-dl}, \fB--extractor \fR{json,mpv,youtube-dl}
playlist extractor, default is \fIyoutube-dl
.TP
.B -c \fICONFIG, \fB--config \fICONFIG
path to the configuration file
.TP
.B --vid \fIVID
initial video channel. \fIauto\fR selects the default, \fIno\fR disables video
.TP
.B --vo \fIDRIVER
specify the video output backend to be used. See
.I VIDEO OUTPUT DRIVERS
in
.BR mpv (1)
for details and descriptions of available drivers
.TP
.B -f \fIYTDL_FORMAT\fR, \fB--format \fIYTDL_FORMAT
video format/quality to be passed to youtube-dl
.SH KEYBOARD CONTROL
.TP
.B Return
start playing
.TP
.B Space
select the current track
.TP
.B /, ?
search forward/backward for a pattern
.TP
.B <, >
go backward/forward in the playlist
.TP
.B A
toggle mute
.TP
.B N
repeat previous search in reverse direction
.TP
.B V
toggle video
.TP
.B W
save the current playlist under JSON format
.TP
.B d
delete current entry
.TP
.B i
insert playlist
.TP
.B m, M
cycle forward/backward through playing modes
.TP
.B n
repeat previous search
.TP
.B p
toggle pause
.TP
.B o
open playlist
.TP
.B w
download tracks set by playing mode
.TP
.B Up, k
move a single line up
.TP
.B Down, j
move a single line down
.TP
.B Left, h
seek backward 5 seconds
.TP
.B Right, l
seek forward 5 seconds
.TP
.B Home
move to the beginning of the playlist
.TP
.B End
move to the end of the playlist
.TP
.B Page Up
move a single page up
.TP
.B Page Down
move a single page down
.TP
.B F5
redraw the screen content
.SH FILES
.TP
.I ~/.config/comp/settings.ini
per user configuration file
.SH EXAMPLES
.nf R
Open a JSON playlist:
.ft B
comp -e json test/playlist.json
.ft R
Open a Youtube playlist with video height lower than 720:
.ft B
comp -f [height<720] https://www.youtube.com/list=PLnk14Iku8QM7R3ARnrj1TwYSZleF-i7jT
.SH BUGS
.PP
Media durations are not extracted from online playlists as youtube-dl
extract_flat option is used. This is rather a feature to save up bandwidth than
a bug because a track's duration is updated when it's played.
.SH AUTHOR
Written by Nguyễn Gia Phong.
.SH "SEE ALSO"
.BR mpv (1),
.BR youtube-dl (1)

View File

@ -18,6 +18,7 @@
import json
from collections import deque
from gettext import bindtextdomain, gettext as _, textdomain
from itertools import cycle
from os.path import abspath, expanduser, expandvars, isfile
from random import choice
@ -25,12 +26,14 @@ from time import gmtime, sleep, strftime
from urllib import request
from youtube_dl import YoutubeDL
from pkg_resources import resource_filename
from mpv import MPV, MpvFormat
DEFAULT_ENTRY = {'filename': '', 'title': '', 'duration': '00:00:00',
'error': False, 'playing': False, 'selected': False}
YTDL_OPTS = {'quiet': True, 'default_search': 'ytsearch',
'extract_flat': 'in_playlist'}
from .ie import extract_info
# Init gettext
bindtextdomain('omp', resource_filename('omp', 'locale'))
textdomain('omp')
class Omp(object):
@ -48,8 +51,14 @@ class Omp(object):
playlist (iterator): iterator of tracks according to mode
search_res (iterator): title-searched results
vid (str): flag show if video output is enabled
I/O handlers (defined by front-end):
print_msg(message, error=False): print a message
property_handler(name, val): called when a mpv property updated
read_input(prompt): prompt for user input
refresh(): update interface content
"""
def __new__(cls, entries, handler, json_file, mode, mpv_vid, mpv_vo, ytdlf):
def __new__(cls, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
self = super(Comp, cls).__new__(cls)
self.play_backward, self.reading = False, False
self.playing = -1
@ -60,13 +69,12 @@ class Omp(object):
ytdl=True, ytdl_format=ytdlf)
return self
def __init__(self, entries, handler, json_file, mode,
mpv_vid, mpv_vo, ytdlf):
def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
if mpv_vo is not None: self.mp['vo'] = mpv_vo
self.mp.observe_property('mute', handler)
self.mp.observe_property('pause', handler)
self.mp.observe_property('time-pos', handler,
force_fmt=MpvFormat.INT64)
@self.mp.property_observer('mute')
@self.mp.property_observer('pause')
@self.mp.property_observer('time-pos', force_fmt=MpvFormat.INT64)
def observer(name, value): self.property_handler(name, value)
def __enter__(self): return self
@ -130,5 +138,21 @@ class Omp(object):
else:
self.update_status(_("Pattern not found"), curses.color_pair(1))
def dump_json(self):
s = self.read_input(
_("Save playlist to [{}]: ").format(self.json_file))
if s: self.json_file = abspath(expanduser(expandvars(s)))
try:
makedirs(dirname(comp.json_file), exist_ok=True)
with open(self.json_file, 'w') as f:
json.dump(self.entries, f, ensure_ascii=False,
indent=2, sort_keys=True)
except:
errmsg = _("'{}': Can't open file for writing").format(
self.json_file)
self.print_msg(errmsg, error=True)
else:
self.print_msg(_("'{}' written").format(comp.json_file))
def __exit__(self, exc_type, exc_value, traceback):
self.mp.quit()

View File

@ -1,8 +1,5 @@
#!/usr/bin/env python3
from os import listdir
from os.path import join
from setuptools import setup
with open('README.rst') as f:
@ -10,7 +7,7 @@ with open('README.rst') as f:
setup(
name='comp',
version='0.3.5',
version='0.3.6',
description=('Curses Omni Media Player'),
long_description=long_description,
url='https://github.com/McSinyx/comp',
@ -32,6 +29,8 @@ setup(
keywords='youtube-dl mpv-wrapper curses console-application multimedia',
packages=['omp'],
install_requires=['python-mpv', 'youtube-dl'],
python_requires='>=3.5',
package_data={'omp': ['locale/*/LC_MESSAGES/omp.mo']},
data_files=[('share/man/man1', ['doc/comp.1'])],
scripts=['comp'],
platforms=['POSIX'])