Update documentations and clean up (?)
This commit is contained in:
parent
21937f383c
commit
0066d60a4e
172
README.rst
172
README.rst
|
@ -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
75
comp
|
@ -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
|
||||
|
|
|
@ -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)
|
46
omp/omp.py
46
omp/omp.py
|
@ -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()
|
||||
|
|
7
setup.py
7
setup.py
|
@ -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'])
|
||||
|
|
Loading…
Reference in New Issue