Compare commits
47 Commits
Author | SHA1 | Date |
---|---|---|
Nguyễn Gia Phong | e860f1526d | |
Nguyễn Gia Phong | 6576c01801 | |
cclauss | 900a3f359a | |
Nguyễn Gia Phong | 7203eaaef5 | |
cclauss | 51ba038b80 | |
Nguyễn Gia Phong | be0eaeadb7 | |
Nguyễn Gia Phong | f8185d1b31 | |
Nguyễn Gia Phong | 082685cee3 | |
Nguyễn Gia Phong | 0500d98b0a | |
Nguyễn Gia Phong | bb29631789 | |
Nguyễn Gia Phong | f4791e6e99 | |
Nguyễn Gia Phong | 37f92bcc74 | |
Nguyễn Gia Phong | e21fd6285d | |
Nguyễn Gia Phong | 4b18b5c1bf | |
Nguyễn Gia Phong | 6ada63f856 | |
Nguyễn Gia Phong | b01b6abd1a | |
Nguyễn Gia Phong | 7542fb3892 | |
Nguyễn Gia Phong | 1b0692417f | |
Nguyễn Gia Phong | ba3a065006 | |
Nguyễn Gia Phong | 04901f33d9 | |
Nguyễn Gia Phong | 9cef1e2382 | |
Nguyễn Gia Phong | 2972111b59 | |
Nguyễn Gia Phong | 6d0aa7fe51 | |
Nguyễn Gia Phong | 801c439146 | |
Nguyễn Gia Phong | 031f9ea1aa | |
Nguyễn Gia Phong | 654c5572ef | |
Nguyễn Gia Phong | 9129a8974f | |
Nguyễn Gia Phong | 98c73ae8ac | |
Nguyễn Gia Phong | 0bca71fe0c | |
Nguyễn Gia Phong | 8334263141 | |
Nguyễn Gia Phong | 056727768d | |
Nguyễn Gia Phong | d146f5d74c | |
Nguyễn Gia Phong | df98e90366 | |
Nguyễn Gia Phong | cf9cfd3c01 | |
Nguyễn Gia Phong | fc6b4a0c51 | |
Nguyễn Gia Phong | c1b0652078 | |
Nguyễn Gia Phong | 6a2c20e490 | |
Nguyễn Gia Phong | 3679965578 | |
Nguyễn Gia Phong | 23fc9ae49b | |
Nguyễn Gia Phong | 5abe0b2ebc | |
Nguyễn Gia Phong | 3f603ed693 | |
Nguyễn Gia Phong | d5117c65c8 | |
Nguyễn Gia Phong | 1b130724d2 | |
Nguyễn Gia Phong | c9fcb30f75 | |
Nguyễn Gia Phong | 1e1418f8dd | |
Nguyễn Gia Phong | 7066eeb697 | |
jaseg | f8b6ac8f66 |
238
README.rst
238
README.rst
|
@ -73,80 +73,196 @@ Open a Youtube playlist with video height lower than 720::
|
|||
Keyboard control
|
||||
----------------
|
||||
|
||||
Bindings inherited from mpv
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
For convenience purpose, I try to mimic **mpv** default keybindings, but many
|
||||
are slightly different from **mpv** exact behaviour (mainly because of the lack
|
||||
of keys which are unsupported by ``curses``). So I will list all of them here
|
||||
for you to `compare <https://github.com/mpv-player/mpv/blob/master/DOCS/man/mpv.rst#keyboard-control>`_:
|
||||
|
||||
Left and Right
|
||||
Seek backward/forward 5 seconds. Shifted arrow does a 1 second seek.
|
||||
|
||||
Up and Down
|
||||
Seek backward/forward 1 minute.
|
||||
|
||||
``[`` and ``]``
|
||||
Decrease/increase current playback speed by 10%.
|
||||
|
||||
``{`` and ``}``
|
||||
Halve/double current playback speed.
|
||||
|
||||
Backspace
|
||||
Reset playback speed to normal.
|
||||
|
||||
``<`` and ``>``
|
||||
Go backward/forward in the playlist.
|
||||
|
||||
Return
|
||||
Start playing.
|
||||
|
||||
Space, ``p``
|
||||
Toggle pause.
|
||||
Space / ``p``
|
||||
Pause (pressing again unpauses).
|
||||
|
||||
``/``, ``?``
|
||||
Search forward/backward for a pattern.
|
||||
``.``
|
||||
Step forward. Pressing once will pause, every consecutive press will play
|
||||
one frame and then go into pause mode again.
|
||||
|
||||
``<``, ``>``
|
||||
Go backward/forward in the playlist.
|
||||
``,``
|
||||
Step backward. Pressing once will pause, every consecutive press will play
|
||||
one frame in reverse and then go into pause mode again.
|
||||
|
||||
``A``
|
||||
Toggle mute.
|
||||
``q``
|
||||
Stop playing and quit.
|
||||
|
||||
``D``
|
||||
Delete the current entry.
|
||||
``/`` / ``9`` and ``*`` / ``0``
|
||||
Decrease/increase volume.
|
||||
|
||||
``N``
|
||||
Repeat previous search in reverse direction.
|
||||
``m``
|
||||
Mute sound.
|
||||
|
||||
``_``
|
||||
Cycle through the available video tracks.
|
||||
|
||||
``#``
|
||||
Cycle through the available audio tracks.
|
||||
|
||||
``f``
|
||||
Toggle fullscreen.
|
||||
|
||||
``T``
|
||||
Toggle stay-on-top.
|
||||
|
||||
``w`` and ``e``
|
||||
Decrease/increase pan-and-scan range.
|
||||
|
||||
``o`` / ``P``
|
||||
Show progression bar, elapsed time and total duration on the OSD.
|
||||
|
||||
``O``
|
||||
Toggle OSD states between normal and playback time/duration.
|
||||
|
||||
``v``
|
||||
Toggle subtitle visibility.
|
||||
|
||||
``j`` and ``J``
|
||||
Cycle through the available subtitles.
|
||||
|
||||
``x`` and ``z``
|
||||
Adjust subtitle delay by +/- 0.1 seconds.
|
||||
|
||||
``l``
|
||||
Set/clear A-B loop points.
|
||||
|
||||
``L``
|
||||
Toggle infinite looping.
|
||||
|
||||
Ctrl-``+`` and Ctrl-``-``
|
||||
Adjust audio delay (A/V sync) by +/- 0.1 seconds.
|
||||
|
||||
``u``
|
||||
Switch between applying no style overrides to SSA/ASS subtitles, and
|
||||
overriding them almost completely with the normal subtitle style.
|
||||
|
||||
``V``
|
||||
Toggle video.
|
||||
Toggle subtitle VSFilter aspect compatibility mode.
|
||||
|
||||
``r`` and ``t``
|
||||
Move subtitles up/down.
|
||||
|
||||
``s``
|
||||
Take a screenshot.
|
||||
|
||||
``S``
|
||||
Take a screenshot, without subtitles.
|
||||
|
||||
Alt-``s``
|
||||
Take a screenshot each frame.
|
||||
|
||||
Page Up and Page Down
|
||||
Seek to the beginning of the previous/next chapter.
|
||||
|
||||
``d``
|
||||
Activate/deactivate deinterlacer.
|
||||
|
||||
``A``
|
||||
Cycle aspect ratio override.
|
||||
|
||||
``1`` and ``2``
|
||||
Adjust contrast.
|
||||
|
||||
``3`` and ``4``
|
||||
Adjust brightness.
|
||||
|
||||
``5`` and ``6``
|
||||
Adjust gamma.
|
||||
|
||||
``7`` and ``8``
|
||||
Adjust saturation.
|
||||
|
||||
Alt-``0``
|
||||
Resize video window to half its original size.
|
||||
|
||||
Alt-``1``
|
||||
Resize video window to its original size.
|
||||
|
||||
Alt-``2``
|
||||
Resize video window to double its original size.
|
||||
|
||||
``E``
|
||||
Cycle through editions.
|
||||
|
||||
Movements and selections
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The following keybindings are Emacs-like since most characters are taken by
|
||||
**mpv**.
|
||||
|
||||
Ctrl-``p`` and Ctrl-``n``
|
||||
Move a single line up/down.
|
||||
|
||||
Alt-``v`` and Ctrl-``v``
|
||||
Move a single page up/down.
|
||||
|
||||
Home / Ctrl-``<`` and End / Ctrl-``>``
|
||||
Move to the beginning/end of the playlist.
|
||||
|
||||
Ctrl-Space
|
||||
Deselect/reselect the current entry and move down a line.
|
||||
|
||||
Playlist manipulation
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Ctrl-``o``
|
||||
Open playlist.
|
||||
|
||||
Ctrl-``i``
|
||||
Insert playlist.
|
||||
|
||||
Ctrl-``f`` and Alt-``f``
|
||||
Search forward/backward for a pattern.
|
||||
|
||||
Alt-``m``
|
||||
Cycle through playing modes.
|
||||
|
||||
Delete
|
||||
Delete the current entry.
|
||||
|
||||
``W``
|
||||
Save the current playlist under JSON format.
|
||||
|
||||
``d``
|
||||
Deselect/reselect the current entry.
|
||||
|
||||
``i``
|
||||
Insert playlist.
|
||||
|
||||
``m``, ``M``
|
||||
Cycle forward/backward through playing modes.
|
||||
|
||||
``n``
|
||||
Repeat previous search.
|
||||
|
||||
``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.
|
||||
|
||||
``:``
|
||||
Execute a **mpv** command.
|
||||
|
||||
Configuration files
|
||||
-------------------
|
||||
|
||||
If not specified by the ``--config``, (user-specific) configuration file is
|
||||
``~/.config/mpv/settings.ini``. Default configurations
|
||||
``~/.config/comp/settings.ini``. Default configurations
|
||||
are listed below::
|
||||
|
||||
[comp]
|
||||
|
@ -156,16 +272,20 @@ are listed below::
|
|||
play-mode = play-current
|
||||
|
||||
[mpv]
|
||||
# Initial video channel. auto selects the default, no disables video.
|
||||
video = auto
|
||||
# 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 =
|
||||
# Options to be parsed to mpv. See OPTIONS section on mpv(1) man pages for
|
||||
# its complete list of available options.
|
||||
# For example:
|
||||
#vo = xv
|
||||
#ontop = yes
|
||||
#border = no
|
||||
#force-window = yes
|
||||
#autofit = 500x280
|
||||
#geometry = -15-50
|
||||
|
||||
[youtube-dl]
|
||||
# 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 = bestvideo+bestaudio
|
||||
|
||||
|
||||
Bugs
|
||||
|
|
329
comp
329
comp
|
@ -16,21 +16,22 @@
|
|||
#
|
||||
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
|
||||
|
||||
__version__ = '0.4.6'
|
||||
|
||||
import curses
|
||||
import json
|
||||
import re
|
||||
from argparse import ArgumentParser
|
||||
from collections import deque
|
||||
from configparser import ConfigParser
|
||||
from curses.ascii import ctrl, alt
|
||||
from functools import reduce
|
||||
from gettext import bindtextdomain, gettext as _, textdomain
|
||||
from os import makedirs
|
||||
from os.path import abspath, dirname, expanduser, expandvars
|
||||
from os.path import expanduser
|
||||
from threading import Thread
|
||||
from traceback import print_exception
|
||||
|
||||
from mpv import MPV
|
||||
from pkg_resources import resource_filename
|
||||
from youtube_dl import YoutubeDL
|
||||
|
||||
from omp import extract_info, Omp
|
||||
|
||||
|
@ -66,19 +67,18 @@ class Comp(Omp):
|
|||
playing (int): index of playing track in played
|
||||
playlist (iterator): iterator of tracks according to mode
|
||||
reading (bool): flag show if user input is being read
|
||||
search_res (iterator): title-searched results
|
||||
search_str (str): regex search string
|
||||
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
|
||||
y (int): the current y-coordinate
|
||||
"""
|
||||
def __new__(cls, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
|
||||
def __new__(cls, entries, json_file, mode, mpv_args, ytdlf):
|
||||
self = object.__new__(cls)
|
||||
self.play_backward, self.reading = False, False
|
||||
self.playing, self.start, self.y = -1, 0, 1
|
||||
self.json_file, self.mode, self.vid = json_file, mode, mpv_vid
|
||||
self.json_file, self.mode = json_file, mode
|
||||
self.entries, self.played = entries, []
|
||||
self.playlist, self.search_res = iter(()), deque()
|
||||
self.playlist, self.search_str = iter(()), ''
|
||||
self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
|
||||
ytdl=True, ytdl_format=ytdlf)
|
||||
self.scr = curses.initscr()
|
||||
|
@ -90,6 +90,7 @@ class Comp(Omp):
|
|||
previously on the display.
|
||||
"""
|
||||
if self.reading: return
|
||||
curses.update_lines_cols()
|
||||
y %= curses.LINES
|
||||
x %= curses.COLS
|
||||
length = X % curses.COLS - x + (y != curses.LINES - 1)
|
||||
|
@ -103,8 +104,8 @@ class Comp(Omp):
|
|||
if self.mp.osd.duration is not None:
|
||||
self.played[self.playing]['duration'] = self.mp.osd.duration
|
||||
add_status_str(':', X=5, lpad=3)
|
||||
if self.vid != 'no': add_status_str('V', x=1, X=2)
|
||||
if not self.mp.mute: add_status_str('A', X=1)
|
||||
if self.mp.video: add_status_str('V', x=1, X=2)
|
||||
if self.mp.audio: add_status_str('A', X=1)
|
||||
add_status_str(self.mp.osd.time_pos or '00:00:00', x=4, X=12)
|
||||
add_status_str('/', x=13, X=14)
|
||||
add_status_str(self.mp.osd.duration or '00:00:00', x=15, X=23)
|
||||
|
@ -115,6 +116,7 @@ class Comp(Omp):
|
|||
self.scr.refresh()
|
||||
|
||||
def print_msg(self, message, error=False):
|
||||
"""Print the given message, in red is it's an error."""
|
||||
attributes = curses.color_pair(1) if error else curses.A_NORMAL
|
||||
self.adds(message, curses.LINES-1, attr=attributes, lpad=0)
|
||||
self.scr.refresh()
|
||||
|
@ -122,15 +124,13 @@ class Comp(Omp):
|
|||
def setno(self, *keys):
|
||||
"""Set all keys of each entry in entries to False."""
|
||||
for entry in self.entries:
|
||||
for key in keys:
|
||||
entry[key] = False
|
||||
for key in keys: entry[key] = False
|
||||
|
||||
def play(self, force=False):
|
||||
"""Play the next track."""
|
||||
def mpv_play(entry, force):
|
||||
self.setno('playing')
|
||||
entry['playing'] = True
|
||||
self.mp.vid = self.vid
|
||||
try:
|
||||
self.mp.play(entry['filename'])
|
||||
except:
|
||||
|
@ -198,7 +198,7 @@ class Comp(Omp):
|
|||
|
||||
def property_handler(self, name, val): self.update_status()
|
||||
|
||||
def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
|
||||
def __init__(self, entries, json_file, mode, mpv_args, ytdlf):
|
||||
curses.noecho()
|
||||
curses.cbreak()
|
||||
self.scr.keypad(True)
|
||||
|
@ -208,24 +208,11 @@ 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)
|
||||
Omp.__init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf)
|
||||
Omp.__init__(self, entries, json_file, mode, mpv_args, ytdlf)
|
||||
self.refresh()
|
||||
|
||||
def __enter__(self): return self
|
||||
|
||||
def idx(self, entry=None):
|
||||
"""Return the index of the current entry."""
|
||||
if entry is None:
|
||||
return self.start + self.y - 1
|
||||
return self.entries.index(entry)
|
||||
|
||||
def current(self):
|
||||
"""Return the current entry."""
|
||||
try:
|
||||
return self.entries[self.idx()]
|
||||
except:
|
||||
return {}
|
||||
|
||||
def read_input(self, prompt):
|
||||
"""Print the prompt string at the bottom of the screen then read
|
||||
from standard input.
|
||||
|
@ -267,24 +254,21 @@ class Comp(Omp):
|
|||
|
||||
def search(self, backward=False):
|
||||
"""Prompt then search for a pattern."""
|
||||
p = re.compile(self.read_input('/'), re.IGNORECASE)
|
||||
s = self.read_input(_("Search {}ward [{{}}]: ".format(
|
||||
'back' if backward else 'for')).format(self.search_str))
|
||||
if s: self.search_str = s
|
||||
pattern = re.compile(self.search_str, re.IGNORECASE)
|
||||
entries = deque(self.entries)
|
||||
entries.rotate(-self.idx())
|
||||
self.search_res = deque(filter(
|
||||
lambda entry: p.search(entry['title']) is not None, entries))
|
||||
if backward: self.search_res.reverse()
|
||||
if self.search_res:
|
||||
self.move(self.idx(self.search_res[0]) - self.idx())
|
||||
if backward:
|
||||
entries.rotate(-self.idx())
|
||||
entries.reverse()
|
||||
else:
|
||||
self.print_msg(_("Pattern not found"), error=True)
|
||||
|
||||
def next_search(self, backward=False):
|
||||
"""Repeat previous search."""
|
||||
if self.search_res:
|
||||
self.search_res.rotate(1 if backward else -1)
|
||||
self.move(self.idx(self.search_res[0]) - self.idx())
|
||||
else:
|
||||
self.print_msg(_("Pattern not found"), error=True)
|
||||
entries.rotate(-self.idx() - 1)
|
||||
for entry in entries:
|
||||
if pattern.search(entry['title']) is not None:
|
||||
self.move(self.idx(entry) - self.idx())
|
||||
return
|
||||
self.print_msg(_("'{}' not found").format(self.search_str), error=True)
|
||||
|
||||
def resize(self):
|
||||
curses.update_lines_cols()
|
||||
|
@ -315,20 +299,19 @@ class Comp(Omp):
|
|||
curses.echo()
|
||||
curses.endwin()
|
||||
Omp.__exit__(self, exc_type, exc_value, traceback)
|
||||
if exc_value is not None:
|
||||
print_exception(exc_type, exc_value, traceback)
|
||||
|
||||
|
||||
parser = ArgumentParser(description='Curses Omni Media Player')
|
||||
parser.add_argument('-v', '--version', action='version',
|
||||
version='%(prog)s 0.3.11')
|
||||
version='%(prog)s {}'.format(__version__))
|
||||
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('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,
|
||||
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) for details and\
|
||||
|
@ -343,22 +326,54 @@ if entries is None:
|
|||
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')
|
||||
vo = args.vo or config.get('mpv', 'video-output', fallback=None)
|
||||
mode = config.get('comp', 'play-mode', fallback='play-current')
|
||||
ytdlf = args.format or config.get('youtube-dl', 'format', fallback='best')
|
||||
mpv_args = dict(config['mpv']) if 'mpv' in config else {}
|
||||
if args.vo is not None: mpv_args['vo'] = args.vo
|
||||
ytdlf = args.format or config.get('youtube-dl', 'format',
|
||||
fallback='bestvideo+bestaudio')
|
||||
|
||||
with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
|
||||
c = comp.scr.getch()
|
||||
while c != 113: # letter q
|
||||
if c == 10: # curses.KEY_ENTER doesn't work
|
||||
comp.update_playlist()
|
||||
comp.next(force=True)
|
||||
elif c in (32, 112): # space or letter p
|
||||
comp.mp.pause ^= True
|
||||
elif c == 47: # /
|
||||
comp.search()
|
||||
elif c == 60: # <
|
||||
with Comp(entries, json_file, mode, mpv_args, ytdlf) as comp:
|
||||
while True:
|
||||
c = comp.scr.get_wch()
|
||||
comp.print_msg('')
|
||||
# mpv keybindings
|
||||
if c == curses.KEY_LEFT:
|
||||
comp.seek(-5, precision='exact')
|
||||
elif c == curses.KEY_RIGHT:
|
||||
comp.seek(5, precision='exact')
|
||||
elif c == curses.KEY_SLEFT: # Shifted Left-arrow
|
||||
comp.seek(-1, precision='exact')
|
||||
elif c == curses.KEY_SRIGHT: # Shifted Right-arrow
|
||||
comp.seek(1, precision='exact')
|
||||
elif c == curses.KEY_UP:
|
||||
comp.seek(-60, precision='exact')
|
||||
elif c == curses.KEY_DOWN:
|
||||
comp.seek(60, precision='exact')
|
||||
elif c == curses.KEY_PPAGE:
|
||||
comp.add('chapter', 1)
|
||||
elif c == curses.KEY_NPAGE:
|
||||
comp.add('chapter', -1)
|
||||
elif c == '[':
|
||||
comp.multiply('speed', 0.9091)
|
||||
elif c == ']':
|
||||
comp.multiply('speed', 1.1)
|
||||
elif c == '{':
|
||||
comp.multiply('speed', 0.5)
|
||||
elif c == '}':
|
||||
comp.multiply('speed', 2.0)
|
||||
elif c == curses.KEY_BACKSPACE:
|
||||
comp.mp.speed = 1.0
|
||||
elif c == 'q':
|
||||
comp.print_msg(_("Save playlist? [Y/n]"))
|
||||
if comp.scr.get_wch() not in _("Nn"): comp.dump_json()
|
||||
break
|
||||
elif c in ('p', ' '):
|
||||
comp.cycle('pause')
|
||||
elif c == '.':
|
||||
comp.mp.frame_step()
|
||||
elif c == ',':
|
||||
comp.mp.frame_back_step()
|
||||
elif c == '<':
|
||||
try:
|
||||
if comp.mp.time_pos < 1:
|
||||
comp.next(backward=True)
|
||||
|
@ -366,34 +381,123 @@ with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
|
|||
comp.seek(0, 'absolute')
|
||||
except:
|
||||
pass
|
||||
elif c == 62: # >
|
||||
elif c == '>':
|
||||
comp.next()
|
||||
elif c == 63: # ?
|
||||
comp.search(backward=True)
|
||||
elif c == 65: # letter A
|
||||
comp.mp.mute ^= True # hack to toggle bool value
|
||||
elif c == 68: # 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.refresh()
|
||||
elif c == 77: # letter M
|
||||
comp.mode = MODES[(MODES.index(comp.mode) - 1) % 8]
|
||||
comp.update_status()
|
||||
elif c == 78: # letter N
|
||||
comp.next_search(backward=True)
|
||||
elif c == 86: # letter V
|
||||
comp.vid = 'auto' if comp.vid == 'no' else 'no'
|
||||
comp.mp.vid = comp.vid
|
||||
comp.update_status()
|
||||
elif c == 87: # letter W
|
||||
comp.dump_json()
|
||||
elif c == 100: # letter d
|
||||
elif c == '\n': # curses.KEY_ENTER doesn't work
|
||||
comp.update_playlist()
|
||||
comp.next(force=True)
|
||||
elif c == 'O':
|
||||
comp.mp.command('cycle-values', 'osd-level', 3, 1)
|
||||
elif c in ('o', 'P'):
|
||||
comp.mp.show_progress()
|
||||
elif c == 'z':
|
||||
comp.add('sub-delay', -0.1)
|
||||
elif c == 'x':
|
||||
comp.add('sub-delay', 0.1)
|
||||
elif c == ctrl('+'):
|
||||
comp.add('audio-delay', 0.1)
|
||||
elif c == ctrl('-'):
|
||||
comp.add('audio-delay', -0.1)
|
||||
elif c in ('/', '9'):
|
||||
comp.add('volume', -2)
|
||||
elif c in ('*', '0'):
|
||||
comp.add('volume', 2)
|
||||
elif c == 'm':
|
||||
comp.cycle('mute')
|
||||
elif c == '1':
|
||||
comp.add('contrast', -1)
|
||||
elif c == '2':
|
||||
comp.add('contrast', 1)
|
||||
elif c == '3':
|
||||
comp.add('brightness', -1)
|
||||
elif c == '4':
|
||||
comp.add('brightness', 1)
|
||||
elif c == '5':
|
||||
comp.add('gamma', -1)
|
||||
elif c == '6':
|
||||
comp.add('gamma', 1)
|
||||
elif c == '7':
|
||||
comp.add('saturation', -1)
|
||||
elif c == '8':
|
||||
comp.add('saturation', 1)
|
||||
elif c == alt('0'):
|
||||
comp.mp.window_scale = 0.5
|
||||
elif c == alt('1'):
|
||||
comp.mp.window_scale = 1.0
|
||||
elif c == alt('2'):
|
||||
comp.mp.window_scale = 2.0
|
||||
elif c == 'd':
|
||||
comp.cycle('deinterlace')
|
||||
elif c == 'r':
|
||||
comp.add('sub-pos', -1)
|
||||
elif c == 't':
|
||||
comp.add('sub-pos', 1)
|
||||
elif c == 'v':
|
||||
comp.cycle('sub-visibility')
|
||||
elif c == 'V':
|
||||
comp.cycle('sub-ass-vsfilter-aspect-compat')
|
||||
elif c == 'u':
|
||||
comp.mp.command('cycle-values', 'sub-ass-override', 'force', 'no')
|
||||
elif c == 'j':
|
||||
comp.cycle('sub', 'up')
|
||||
elif c == 'J':
|
||||
comp.cycle('sub', 'down')
|
||||
elif c == '#':
|
||||
comp.cycle('audio')
|
||||
elif c == '_':
|
||||
comp.cycle('video')
|
||||
elif c == 'T':
|
||||
comp.cycle('ontop')
|
||||
elif c == 'f':
|
||||
comp.cycle('fullscreen')
|
||||
elif c == 's':
|
||||
comp.mp.screenshot()
|
||||
elif c == 'S':
|
||||
comp.mp.screenshot(includes='')
|
||||
elif c == alt('s'):
|
||||
comp.mp.screenshot(mode='each-frame')
|
||||
elif c == 'w':
|
||||
comp.add('panscan', -0.1)
|
||||
elif c == 'e':
|
||||
comp.add('panscan', 0.1)
|
||||
elif c == 'A':
|
||||
comp.mp.command('cycle-values', 'video-aspect',
|
||||
'16:9', '4:3', '2.35:1', '-1')
|
||||
elif c == 'E':
|
||||
comp.cycle('edition')
|
||||
elif c == 'l':
|
||||
comp.mp.command('ab-loop')
|
||||
elif c == 'L':
|
||||
comp.mp.command('cycle-values', 'loop-file', 'inf', 'no')
|
||||
|
||||
# Emacs keybindings
|
||||
elif c == ctrl('p'):
|
||||
comp.move(-1)
|
||||
elif c == ctrl('n'):
|
||||
comp.move(1)
|
||||
elif c == alt('v'):
|
||||
comp.move(4 - curses.LINES)
|
||||
elif c == ctrl('v'):
|
||||
comp.move(curses.LINES - 4)
|
||||
elif c in (ctrl('<'), curses.KEY_HOME):
|
||||
comp.move(-len(comp.entries))
|
||||
elif c in (ctrl('>'), curses.KEY_END):
|
||||
comp.move(len(comp.entries))
|
||||
elif c == ctrl(' '):
|
||||
comp.current()['selected'] = not comp.current().get('selected')
|
||||
comp.move(1)
|
||||
elif c == 105: # letter i
|
||||
|
||||
elif c == ctrl('o'):
|
||||
extractor = comp.read_input(_("Playlist extractor: "))
|
||||
filename = comp.read_input(_("Open: "))
|
||||
entries = extract_info(filename, extractor)
|
||||
if entries is None:
|
||||
comp.print_msg(
|
||||
_("'{}': Can't extract playlist").format(filename))
|
||||
else:
|
||||
comp.entries, comp.start, comp.y = entries, 0, 1
|
||||
comp.refresh()
|
||||
elif c == ctrl('i'):
|
||||
extractor = comp.read_input(_("Playlist extractor: "))
|
||||
filename = comp.read_input(_("Insert: "))
|
||||
entries = extract_info(filename, extractor)
|
||||
|
@ -406,37 +510,26 @@ with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
|
|||
comp.entries.extend(entries)
|
||||
comp.entries.extend(bottom)
|
||||
comp.refresh()
|
||||
elif c == 109: # letter m
|
||||
elif c == ctrl('f'):
|
||||
comp.search()
|
||||
elif c == alt('f'):
|
||||
comp.search(backward=True)
|
||||
elif c == alt('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.read_input(_("Playlist extractor: "))
|
||||
filename = comp.read_input(_("Open: "))
|
||||
entries = extract_info(filename, extractor)
|
||||
if entries is None:
|
||||
comp.print_msg(
|
||||
_("'{}': Can't extract playlist").format(filename))
|
||||
else:
|
||||
comp.entries, comp.start, comp.y = entries, 0, 1
|
||||
comp.refresh()
|
||||
elif c in (curses.KEY_UP, 107): # up arrow or letter k
|
||||
comp.move(-1)
|
||||
elif c in (curses.KEY_DOWN, 106): # down arrow or letter j
|
||||
comp.move(1)
|
||||
elif c in (curses.KEY_LEFT, 104): # left arrow or letter h
|
||||
comp.seek(-5, precision='exact')
|
||||
elif c in (curses.KEY_RIGHT, 108): # right arrow or letter l
|
||||
comp.seek(5, precision='exact')
|
||||
elif c == curses.KEY_HOME: # home
|
||||
comp.move(-len(comp.entries))
|
||||
elif c == curses.KEY_END: # end
|
||||
comp.move(len(comp.entries))
|
||||
elif c == curses.KEY_NPAGE: # page down
|
||||
comp.move(curses.LINES - 4)
|
||||
elif c == curses.KEY_PPAGE: # page up
|
||||
comp.move(4 - curses.LINES)
|
||||
elif c == curses.KEY_DC:
|
||||
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.refresh()
|
||||
elif c == 'W':
|
||||
comp.dump_json()
|
||||
elif c in (curses.KEY_F5, curses.KEY_RESIZE):
|
||||
comp.resize()
|
||||
c = comp.scr.getch()
|
||||
elif c == ':':
|
||||
try:
|
||||
comp.mp.command(*comp.read_input(':').split())
|
||||
except:
|
||||
comp.print_msg(_("Failed to execute command"), error=True)
|
||||
|
|
229
doc/comp.1
229
doc/comp.1
|
@ -1,7 +1,7 @@
|
|||
.\" Process this file with
|
||||
.\" groff -man -Tutf8 comp.1
|
||||
.\"
|
||||
.TH COMP 1 2017-06-17 comp
|
||||
.TH COMP 1 2018-01-25 comp
|
||||
.SH NAME
|
||||
comp \- Curses Omni Media Player
|
||||
.SH SYNOPSIS
|
||||
|
@ -11,8 +11,10 @@ comp \- Curses Omni Media Player
|
|||
.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
|
||||
front-end using
|
||||
.BR curses (3).
|
||||
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
|
||||
|
@ -47,75 +49,192 @@ for details and descriptions of available drivers
|
|||
.B -f \fIYTDL_FORMAT\fR, \fB--format \fIYTDL_FORMAT
|
||||
video format/quality to be passed to youtube-dl
|
||||
.SH KEYBOARD CONTROL
|
||||
.SS Bindings inherited from mpv
|
||||
For convenience purpose, I try to mimic
|
||||
.BR mpv (1)
|
||||
default keybindings, but many are slightly different from
|
||||
.BR mpv (1)
|
||||
exact behaviour (mainly because of the lack of keys which are unsupported by
|
||||
.BR curses (3)).
|
||||
So I will list all of them here for you to compare:
|
||||
.TP
|
||||
.B Left and Right
|
||||
Seek backward/forward 5 seconds. Shifted arrow does a 1 second seek.
|
||||
.TP
|
||||
.B Up and Down
|
||||
Seek backward/forward 1 minute.
|
||||
.TP
|
||||
.B [ and ]
|
||||
Decrease/increase current playback speed by 10%.
|
||||
.TP
|
||||
.B { and }
|
||||
Halve/double current playback speed.
|
||||
.TP
|
||||
.B Backspace
|
||||
Reset playback speed to normal.
|
||||
.TP
|
||||
.B < and >
|
||||
Go backward/forward in the playlist.
|
||||
.TP
|
||||
.B Return
|
||||
Start playing.
|
||||
.TP
|
||||
.B Space, p
|
||||
Toggle pause.
|
||||
.B Space / p
|
||||
Pause (pressing again unpauses).
|
||||
.TP
|
||||
.B /, ?
|
||||
Search forward/backward for a pattern.
|
||||
.B .
|
||||
Step forward. Pressing once will pause, every consecutive press will play
|
||||
one frame and then go into pause mode again.
|
||||
.TP
|
||||
.B <, >
|
||||
Go backward/forward in the playlist.
|
||||
.B ,
|
||||
Step backward. Pressing once will pause, every consecutive press will play
|
||||
one frame in reverse and then go into pause mode again.
|
||||
.TP
|
||||
.B A
|
||||
Toggle mute.
|
||||
.B q
|
||||
Stop playing and quit.
|
||||
.TP
|
||||
.B D
|
||||
Delete the current entry.
|
||||
.B / and *
|
||||
Decrease/increase volume.
|
||||
.TP
|
||||
.B N
|
||||
Repeat previous search in reverse direction.
|
||||
.B 9 and 0
|
||||
Decrease/increase volume.
|
||||
.TP
|
||||
.B m
|
||||
Mute sound.
|
||||
.TP
|
||||
.B _
|
||||
Cycle through the available video tracks.
|
||||
.TP
|
||||
.B #
|
||||
Cycle through the available audio tracks.
|
||||
.TP
|
||||
.B f
|
||||
Toggle fullscreen.
|
||||
.TP
|
||||
.B T
|
||||
Toggle stay-on-top.
|
||||
.TP
|
||||
.B w and e
|
||||
Decrease/increase pan-and-scan range.
|
||||
.TP
|
||||
.B o or P
|
||||
Show progression bar, elapsed time and total duration on the OSD.
|
||||
.TP
|
||||
.B O
|
||||
Toggle OSD states between normal and playback time/duration.
|
||||
.TP
|
||||
.B v
|
||||
Toggle subtitle visibility.
|
||||
.TP
|
||||
.B j and J
|
||||
Cycle through the available subtitles.
|
||||
.TP
|
||||
.B x and z
|
||||
Adjust subtitle delay by +/- 0.1 seconds.
|
||||
.TP
|
||||
.B l
|
||||
Set/clear A-B loop points.
|
||||
.TP
|
||||
.B L
|
||||
Toggle infinite looping.
|
||||
.TP
|
||||
.B Ctrl-+ and Ctrl--
|
||||
Adjust audio delay (A/V sync) by +/- 0.1 seconds.
|
||||
.TP
|
||||
.B u
|
||||
Switch between applying no style overrides to SSA/ASS subtitles, and
|
||||
overriding them almost completely with the normal subtitle style.
|
||||
.TP
|
||||
.B V
|
||||
Toggle video.
|
||||
Toggle subtitle VSFilter aspect compatibility mode.
|
||||
.TP
|
||||
.B r and t
|
||||
Move subtitles up/down.
|
||||
.TP
|
||||
.B s
|
||||
Take a screenshot.
|
||||
.TP
|
||||
.B S
|
||||
Take a screenshot, without subtitles.
|
||||
.TP
|
||||
.B Alt-s
|
||||
Take a screenshot each frame.
|
||||
.TP
|
||||
.B Page Up and Page Down
|
||||
Seek to the beginning of the previous/next chapter.
|
||||
.TP
|
||||
.B d
|
||||
Activate/deactivate deinterlacer.
|
||||
.TP
|
||||
.B A
|
||||
Cycle aspect ratio override.
|
||||
.TP
|
||||
.B 1 and 2
|
||||
Adjust contrast.
|
||||
.TP
|
||||
.B 3 and 4
|
||||
Adjust brightness.
|
||||
.TP
|
||||
.B 5 and 6
|
||||
Adjust gamma.
|
||||
.TP
|
||||
.B 7 and 8
|
||||
Adjust saturation.
|
||||
.TP
|
||||
.B Alt-0
|
||||
Resize video window to half its original size.
|
||||
.TP
|
||||
.B Alt-1
|
||||
Resize video window to its original size.
|
||||
.TP
|
||||
.B Alt-2
|
||||
Resize video window to double its original size.
|
||||
.TP
|
||||
.B E
|
||||
Cycle through editions.
|
||||
.SS Movements and selections
|
||||
The following keybindings are Emacs-like since most characters are taken by
|
||||
.BR mpv (1).
|
||||
.TP
|
||||
.B Ctrl-p and Ctrl-n
|
||||
Move a single line up/down.
|
||||
.TP
|
||||
.B Alt-v and Ctrl-v
|
||||
Move a single page up/down.
|
||||
.TP
|
||||
.B Ctrl-< and Ctrl->
|
||||
Move to the beginning/end of the playlist.
|
||||
.TP
|
||||
.B Home and End
|
||||
Move to the beginning/end of the playlist.
|
||||
.TP
|
||||
.B Ctrl-Space
|
||||
Deselect/reselect the current entry and move down a line.
|
||||
.SS Playlist manipulation
|
||||
.TP
|
||||
.B Ctrl-o
|
||||
Open playlist.
|
||||
.TP
|
||||
.B Ctrl-i
|
||||
Insert playlist.
|
||||
.TP
|
||||
.B Ctrl-f and Alt-f
|
||||
Search forward/backward for a pattern.
|
||||
.TP
|
||||
.B Alt-m
|
||||
Cycle through playing modes.
|
||||
.TP
|
||||
.B Delete
|
||||
Delete the current entry.
|
||||
.TP
|
||||
.B W
|
||||
Save the current playlist under JSON format.
|
||||
.TP
|
||||
.B d
|
||||
Deselect/reselect the 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 o
|
||||
Open playlist.
|
||||
.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.
|
||||
.TP
|
||||
.B :
|
||||
Execute a mpv command.
|
||||
.SH FILES
|
||||
.TP
|
||||
.I ~/.config/comp/settings.ini
|
||||
|
|
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
# Vietnamese translation for Omp front-ends.
|
||||
# Copyright (C) 2018 Nguyễn Gia Phong
|
||||
# Nguyễn Gia Phong <vn.mcsinyx@gmail.com>, 2018
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
@ -50,6 +50,12 @@ msgstr "Tiêu đề"
|
|||
msgid "Current size: {}x{}. Minimum size: {}x4."
|
||||
msgstr "Kích thước hiện tại: {}x{}. Kích thước tối thiểu: {}x4."
|
||||
|
||||
msgid "Save playlist? [Y/n]"
|
||||
msgstr "Lưu danh sách phát? [C/k]"
|
||||
|
||||
msgid "Nn"
|
||||
msgstr "Kk"
|
||||
|
||||
msgid "Save playlist to [{}]: "
|
||||
msgstr "Lưu danh sách phát tại [{}]: "
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ from mpv import MPV
|
|||
|
||||
DEFAULT_ENTRY = {'filename': '', 'title': '', 'duration': '00:00:00',
|
||||
'error': False, 'playing': False, 'selected': False}
|
||||
JSON_KEYS = 'filename', 'title', 'duration', 'error', 'selected'
|
||||
|
||||
|
||||
class YoutubeDLLogger:
|
||||
|
|
143
omp/omp.py
143
omp/omp.py
|
@ -16,27 +16,37 @@
|
|||
#
|
||||
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
|
||||
|
||||
import curses
|
||||
import json
|
||||
import re
|
||||
from bisect import bisect_left as bisect
|
||||
from collections import deque
|
||||
from gettext import bindtextdomain, gettext as _, textdomain
|
||||
from itertools import cycle
|
||||
from os import makedirs
|
||||
from os.path import abspath, dirname, expanduser, expandvars, isfile
|
||||
from os.path import abspath, dirname, expanduser, expandvars
|
||||
from random import choice
|
||||
from time import gmtime, sleep, strftime
|
||||
from urllib import request
|
||||
from sys import exc_info
|
||||
|
||||
from youtube_dl import YoutubeDL
|
||||
from pkg_resources import resource_filename
|
||||
from mpv import MPV, MpvFormat
|
||||
from mpv import MPV
|
||||
|
||||
from .ie import extract_info
|
||||
from .ie import JSON_KEYS
|
||||
|
||||
# Init gettext
|
||||
bindtextdomain('omp', resource_filename('omp', 'locale'))
|
||||
textdomain('omp')
|
||||
|
||||
|
||||
def shuffle_init(a):
|
||||
"""Return in iterator which yield random elements from a,
|
||||
and always begin with its first element.
|
||||
"""
|
||||
if a:
|
||||
yield a[0]
|
||||
while True: yield choice(a)
|
||||
|
||||
|
||||
class Omp(object):
|
||||
"""Omni Media Player meta object.
|
||||
|
||||
|
@ -51,7 +61,6 @@ class Omp(object):
|
|||
playing (int): index of playing track in played
|
||||
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
|
||||
|
@ -59,19 +68,23 @@ class Omp(object):
|
|||
read_input(prompt): prompt for user input
|
||||
refresh(): update interface content
|
||||
"""
|
||||
def __new__(cls, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
|
||||
self = super(Comp, cls).__new__(cls)
|
||||
def __new__(cls, entries, json_file, mode, mpv_args, ytdlf):
|
||||
self = object.__new__(cls)
|
||||
self.play_backward, self.reading = False, False
|
||||
self.playing = -1
|
||||
self.json_file, self.mode, self.vid = json_file, mode, mpv_vid
|
||||
self.json_file, self.mode = json_file, mode
|
||||
self.entries, self.played = entries, []
|
||||
self.playlist, self.search_res = iter(()), deque()
|
||||
self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
|
||||
ytdl=True, ytdl_format=ytdlf)
|
||||
return self
|
||||
|
||||
def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
|
||||
if mpv_vo is not None: self.mp['vo'] = mpv_vo
|
||||
def __init__(self, entries, json_file, mode, mpv_args, ytdlf):
|
||||
for arg, val in mpv_args.items():
|
||||
try:
|
||||
self.mp[arg] = val
|
||||
except:
|
||||
self.__exit__(*exc_info())
|
||||
@self.mp.property_observer('mute')
|
||||
@self.mp.property_observer('pause')
|
||||
@self.mp.property_observer('time-pos')
|
||||
|
@ -80,43 +93,88 @@ class Omp(object):
|
|||
|
||||
def __enter__(self): return self
|
||||
|
||||
def update_play_list(self, pick):
|
||||
"""Update the list of entries to be played."""
|
||||
if pick == 'current':
|
||||
self.play_list = [self.current()]
|
||||
elif pick == 'all':
|
||||
self.play_list = deque(self.entries)
|
||||
self.play_list.rotate(-self.idx())
|
||||
else:
|
||||
self.play_list = [i for i in self.entries if i.get('selected')]
|
||||
|
||||
def update_playlist(self):
|
||||
"""Update the playlist to be used by play function."""
|
||||
action, pick = self.mode.split('-')
|
||||
self.update_play_list(pick)
|
||||
if action == 'play':
|
||||
self.playlist = iter(self.play_list)
|
||||
elif action == 'repeat':
|
||||
self.playlist = cycle(self.play_list)
|
||||
else:
|
||||
self.playlist = iter(lambda: choice(self.play_list), None)
|
||||
if self.playing < -1: self.played = self.played[:self.playing+1]
|
||||
|
||||
def seek(self, amount, reference='relative', precision='default-precise'):
|
||||
"""Wrap mp.seek with a try clause to avoid crash when nothing is
|
||||
being played.
|
||||
"""Wrap a try clause around mp.seek to avoid crashing when
|
||||
nothing is being played.
|
||||
"""
|
||||
try:
|
||||
self.mp.seek(amount, reference, precision)
|
||||
except:
|
||||
pass
|
||||
self.print_msg(_("Failed to seek"), error=True)
|
||||
|
||||
def add(self, name, value=1):
|
||||
"""Wrap a try clause around mp.property_add."""
|
||||
try:
|
||||
self.mp.property_add(name, value)
|
||||
except:
|
||||
self.print_msg(
|
||||
_("Failed to add {} to '{}'").format(value, name), error=True)
|
||||
|
||||
def multiply(self, name, factor):
|
||||
"""Wrap a try clause around mp.property_multiply."""
|
||||
try:
|
||||
self.mp.property_multiply(name, factor)
|
||||
except:
|
||||
self.print_msg(
|
||||
_("Failed to multiply '{}' with {}").format(name, factor),
|
||||
error=True)
|
||||
|
||||
def cycle(self, name, direction='up'):
|
||||
"""Wrap a try clause around mp.cycle."""
|
||||
try:
|
||||
self.mp.cycle(name, direction='up')
|
||||
except:
|
||||
self.print_msg(
|
||||
_("Failed to cycle {} '{}'").format(direction, name),
|
||||
error=True)
|
||||
|
||||
def idx(self, entry=None):
|
||||
"""Return the index of the current entry."""
|
||||
if entry is None:
|
||||
return self.start + self.y - 1
|
||||
return self.entries.index(entry)
|
||||
|
||||
def current(self):
|
||||
"""Return the current entry."""
|
||||
try:
|
||||
return self.entries[self.idx()]
|
||||
except:
|
||||
return {}
|
||||
|
||||
def update_playlist(self):
|
||||
"""Update the playlist to be used by play function."""
|
||||
action, pick = self.mode.split('-')
|
||||
if pick == 'current':
|
||||
self.play_list = deque([self.current()])
|
||||
elif pick == 'all':
|
||||
self.play_list = deque(self.entries)
|
||||
self.play_list.rotate(-self.idx())
|
||||
elif pick == 'selected':
|
||||
self.play_list = deque([entry for entry in self.entries
|
||||
if entry.get('selected')])
|
||||
indexes = [i for i, entry in enumerate(self.entries)
|
||||
if entry.get('selected')]
|
||||
idx = indexes[bisect(indexes, self.idx())]
|
||||
self.play_list.rotate(-self.play_list.index(self.entries[idx]))
|
||||
|
||||
if action == 'play':
|
||||
self.playlist = iter(self.play_list)
|
||||
elif action == 'repeat':
|
||||
self.playlist = cycle(self.play_list)
|
||||
elif action == 'shuffle':
|
||||
self.playlist = shuffle_init(self.play_list)
|
||||
if self.playing < -1: self.played = self.played[:self.playing+1]
|
||||
|
||||
def next(self, force=False, backward=False):
|
||||
"""Go forward/backward in the playlist.
|
||||
|
||||
If forced, this will also unpause the player.
|
||||
"""
|
||||
self.play_backward = backward
|
||||
if self.mp.idle_active:
|
||||
self.play(force)
|
||||
else:
|
||||
self.seek(100, 'absolute-percent')
|
||||
self.mp.time_pos = self.mp.duration
|
||||
if force: self.mp.pause = False
|
||||
|
||||
def search(self, backward=False):
|
||||
|
@ -141,19 +199,22 @@ class Omp(object):
|
|||
self.update_status(_("Pattern not found"), curses.color_pair(1))
|
||||
|
||||
def dump_json(self):
|
||||
"""Read user input needed to save the playlist."""
|
||||
s = self.read_input(
|
||||
_("Save playlist to [{}]: ").format(self.json_file))
|
||||
self.json_file = abspath(expanduser(expandvars(s or self.json_file)))
|
||||
entries = [{k: v for k, v in entry.items() if k in JSON_KEYS}
|
||||
for entry in self.entries]
|
||||
try:
|
||||
makedirs(dirname(self.json_file), exist_ok=True)
|
||||
with open(self.json_file, 'w') as f:
|
||||
json.dump(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:
|
||||
with open(self.json_file, 'w') as f:
|
||||
json.dump(self.entries, f, ensure_ascii=False,
|
||||
indent=2, sort_keys=True)
|
||||
self.print_msg(_("'{}' written").format(self.json_file))
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
|
|
16
settings.ini
16
settings.ini
|
@ -5,13 +5,17 @@
|
|||
play-mode = play-current
|
||||
|
||||
[mpv]
|
||||
# Initial video channel. auto selects the default, no disables video.
|
||||
video = auto
|
||||
# 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 =
|
||||
# Options to be parsed to mpv. See OPTIONS section on mpv(1) man pages for
|
||||
# its complete list of available options.
|
||||
# For example:
|
||||
#vo = xv
|
||||
#ontop = yes
|
||||
#border = no
|
||||
#force-window = yes
|
||||
#autofit = 500x280
|
||||
#geometry = -15-50
|
||||
|
||||
[youtube-dl]
|
||||
# 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 = bestvideo+bestaudio
|
||||
|
|
2
setup.py
2
setup.py
|
@ -7,7 +7,7 @@ with open('README.rst') as f:
|
|||
|
||||
setup(
|
||||
name='comp',
|
||||
version='0.3.12',
|
||||
version='0.4.6',
|
||||
description=('Curses Omni Media Player'),
|
||||
long_description=long_description,
|
||||
url='https://github.com/McSinyx/comp',
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"duration": "00:05:21",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/weeI1G46q0o",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "DJ Khaled - I'm the One ft. Justin Bieber, Quavo, Chance the Rapper, Lil Wayne"
|
||||
},
|
||||
|
@ -11,127 +10,55 @@
|
|||
"duration": "00:04:23",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/JGwWNGJdvx8",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "Ed Sheeran - Shape of You [Official Video]"
|
||||
},
|
||||
{
|
||||
"duration": "00:03:30",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/PMivT7MJ41M",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "Bruno Mars - That’s What I Like [Official Video]"
|
||||
},
|
||||
{
|
||||
"duration": "00:04:46",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/CTFtOOh47oo",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "French Montana - Unforgettable ft. Swae Lee"
|
||||
},
|
||||
{
|
||||
"duration": "00:04:45",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/NLZRYQMLDW4",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Kendrick Lamar - DNA."
|
||||
},
|
||||
{
|
||||
"duration": "00:04:07",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/FM7MFYoylVs",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "The Chainsmokers & Coldplay - Something Just Like This (Lyric)"
|
||||
},
|
||||
{
|
||||
"duration": "00:03:48",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/72UO0v5ESUo",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "Luis Fonsi, Daddy Yankee - Despacito (Audio) ft. Justin Bieber"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:03:41",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/D5drYkLiLI8",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Kygo, Selena Gomez - It Ain't Me (with Selena Gomez) (Audio)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/Zgmvg-zzctI",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Lil Uzi Vert - XO TOUR Llif3 (Produced By TM88)"
|
||||
},
|
||||
{
|
||||
"duration": "00:12:53",
|
||||
"error": false,
|
||||
"filename": "test/gplv3.ogg",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "gplv3.ogg"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/xvZqHgFz51I",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Future - Mask Off"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/8j9zMok6two",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Miley Cyrus - Malibu (Official Video)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:04:17",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/dPI-mRFEIH0",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Katy Perry - Bon Appétit (Official) ft. Migos"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:04:06",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/aatr_2MstrI",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Clean Bandit - Symphony feat. Zara Larsson [Official Video]"
|
||||
},
|
||||
{
|
||||
"duration": "00:34:38",
|
||||
"error": false,
|
||||
"filename": "https://www.tube8.com/teen/nicole-ray-and-james-deen/409802/",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "Nicole Ray and James Deen"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:04:16",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/7F37r50VUTQ",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "ZAYN, Taylor Swift - I Don’t Wanna Live Forever (Fifty Shades Darker)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:04:57",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/qFLhGq0060w",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "The Weeknd - I Feel It Coming ft. Daft Punk"
|
||||
},
|
||||
|
@ -139,55 +66,41 @@
|
|||
"duration": "00:02:29",
|
||||
"error": false,
|
||||
"filename": "http://www.html5videoplayer.net/videos/toystory.mp4",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "toystory.mp4"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:05:13",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/6ImFf__U6io",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "Birdman - Dark Shades (Explicit) ft. Lil Wayne, Mack Maine"
|
||||
},
|
||||
{
|
||||
"duration": "00:03:56",
|
||||
"duration": "00:03:55",
|
||||
"error": false,
|
||||
"filename": "https://www.youtube.com/watch?v=3M3xfu0m5o4",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "David Banner - Play (Dirty version)"
|
||||
},
|
||||
{
|
||||
"duration": "00:04:14",
|
||||
"error": false,
|
||||
"filename": "https://www.youtube.com/watch?v=NmCFY1oYDeM",
|
||||
"selected": false,
|
||||
"title": "John Legend - Love Me Now (Video)"
|
||||
},
|
||||
{
|
||||
"duration": "00:03:55",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/NGLxoKOvzu4",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Jason Derulo - Swalla (feat. Nicki Minaj & Ty Dolla $ign) (Official Music Video)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/Hm1YFszJWbQ",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Migos - Slippery feat. Gucci Mane [Official Video]"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/SC4xMk98Pdc",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Post Malone - Congratulations ft. Quavo"
|
||||
},
|
||||
{
|
||||
"duration": "00:03:51",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/nfs8NYg7yQM",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Charlie Puth - Attention [Official Video]"
|
||||
},
|
||||
|
@ -195,39 +108,27 @@
|
|||
"duration": "00:04:10",
|
||||
"error": false,
|
||||
"filename": "https://www.youtube.com/watch?v=sRIkXM8S1J8",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "Best Goat Song Versions Compilation Ever! (HD)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/Dst9gZkq1a8",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Travis Scott - goosebumps ft. Kendrick Lamar"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:04:03",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/dMK_npDG12Q",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Lorde - Green Light"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:03:32",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/h--P8HzYZ74",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "Zedd, Alessia Cara - Stay (Lyric Video)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:02:45",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/Mdh2p03cRfw",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Sam Hunt - Body Like A Back Road (Audio)"
|
||||
},
|
||||
|
@ -235,7 +136,6 @@
|
|||
"duration": "00:03:40",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/Fq0xEpRDL9Q",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Chris Brown - Privacy (Explicit Version)"
|
||||
},
|
||||
|
@ -243,87 +143,55 @@
|
|||
"duration": "00:03:36",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/7wtfhZwyrcc",
|
||||
"playing": true,
|
||||
"selected": false,
|
||||
"title": "Imagine Dragons - Believer"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:03:52",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/t_jHrUE5IOk",
|
||||
"playing": false,
|
||||
"filename": "https://youtu.be/A-Rn0iQEpc8",
|
||||
"selected": false,
|
||||
"title": "Maluma - Felices los 4 (Official Video)"
|
||||
"title": "Can't Stop the SUSE - (Can't Stop the Feeling parody)"
|
||||
},
|
||||
{
|
||||
"duration": "00:04:28",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/SYRlTISvjww",
|
||||
"selected": false,
|
||||
"title": "Uptime Funk - (Uptown Funk parody)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/wzZWXrlDj-A",
|
||||
"playing": false,
|
||||
"filename": "https://youtu.be/VNkDJk5_9eU",
|
||||
"selected": false,
|
||||
"title": "DNCE - Kissing Strangers ft. Nicki Minaj"
|
||||
"title": "What Does the Chameleon Say? (Ylvis - What Does the Fox Say parody)"
|
||||
},
|
||||
{
|
||||
"duration": "00:03:18",
|
||||
"duration": "00:03:46",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/AEB6ibtdPZc",
|
||||
"playing": false,
|
||||
"filename": "https://youtu.be/M9bq_alk-sw",
|
||||
"selected": false,
|
||||
"title": "Paramore: Hard Times [OFFICIAL VIDEO]"
|
||||
"title": "SUSE. Yes Please. (Maroon 5 - Sugar parody)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:03:30",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/vqW18C4plZ8",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "WizKid - Come Closer ft. Drake"
|
||||
"filename": "https://youtu.be/oHNKTlz1lps",
|
||||
"selected": true,
|
||||
"title": "Linus Said - Music Parody (Momma Said)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:03:58",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/A7xzXDStQnk",
|
||||
"playing": false,
|
||||
"filename": "https://youtu.be/4VrhlyIgo3M",
|
||||
"selected": false,
|
||||
"title": "Shawn Mendes - There's Nothing Holdin' Me Back (Lyric Video)"
|
||||
"title": "25 Years - SUSE Music Video (7 Years parody)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/FG9M0aEpJGE",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "G-Eazy & Kehlani - Good Life (from The Fate of the Furious: The Album) [MUSIC VIDEO]"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/vp8VZe5kqEM",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Lady Gaga - The Cure (Audio)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/eP4eqhWc7sI",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Lana Del Rey - Lust For Life (Official Video) ft. The Weeknd"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/5qJp6xlKEug",
|
||||
"playing": false,
|
||||
"selected": false,
|
||||
"title": "Gorillaz - Saturnz Barz (Spirit House)"
|
||||
},
|
||||
{
|
||||
"duration": "00:00:00",
|
||||
"duration": "00:03:52",
|
||||
"error": false,
|
||||
"filename": "https://youtu.be/9sg-A-eS6Ig",
|
||||
"playing": false,
|
||||
"selected": true,
|
||||
"title": "Enrique Iglesias - SUBEME LA RADIO (Official Video) ft. Descemer Bueno, Zion & Lennox"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue