Compare commits
47 commits
Author | SHA1 | Date | |
---|---|---|---|
e860f1526d | |||
6576c01801 | |||
|
900a3f359a | ||
7203eaaef5 | |||
|
51ba038b80 | ||
be0eaeadb7 | |||
f8185d1b31 | |||
082685cee3 | |||
0500d98b0a | |||
bb29631789 | |||
f4791e6e99 | |||
37f92bcc74 | |||
e21fd6285d | |||
4b18b5c1bf | |||
6ada63f856 | |||
b01b6abd1a | |||
7542fb3892 | |||
1b0692417f | |||
ba3a065006 | |||
04901f33d9 | |||
9cef1e2382 | |||
2972111b59 | |||
6d0aa7fe51 | |||
801c439146 | |||
031f9ea1aa | |||
654c5572ef | |||
9129a8974f | |||
98c73ae8ac | |||
0bca71fe0c | |||
8334263141 | |||
056727768d | |||
d146f5d74c | |||
df98e90366 | |||
cf9cfd3c01 | |||
fc6b4a0c51 | |||
c1b0652078 | |||
6a2c20e490 | |||
3679965578 | |||
23fc9ae49b | |||
5abe0b2ebc | |||
3f603ed693 | |||
d5117c65c8 | |||
1b130724d2 | |||
c9fcb30f75 | |||
1e1418f8dd | |||
7066eeb697 | |||
|
f8b6ac8f66 |
10 changed files with 727 additions and 455 deletions
238
README.rst
238
README.rst
|
@ -73,80 +73,196 @@ Open a Youtube playlist with video height lower than 720::
|
||||||
Keyboard control
|
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
|
Return
|
||||||
Start playing.
|
Start playing.
|
||||||
|
|
||||||
Space, ``p``
|
Space / ``p``
|
||||||
Toggle pause.
|
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``
|
``q``
|
||||||
Toggle mute.
|
Stop playing and quit.
|
||||||
|
|
||||||
``D``
|
``/`` / ``9`` and ``*`` / ``0``
|
||||||
Delete the current entry.
|
Decrease/increase volume.
|
||||||
|
|
||||||
``N``
|
``m``
|
||||||
Repeat previous search in reverse direction.
|
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``
|
``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``
|
``W``
|
||||||
Save the current playlist under JSON format.
|
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
|
F5
|
||||||
Redraw the screen content.
|
Redraw the screen content.
|
||||||
|
|
||||||
|
``:``
|
||||||
|
Execute a **mpv** command.
|
||||||
|
|
||||||
Configuration files
|
Configuration files
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
If not specified by the ``--config``, (user-specific) configuration file is
|
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::
|
are listed below::
|
||||||
|
|
||||||
[comp]
|
[comp]
|
||||||
|
@ -156,16 +272,20 @@ are listed below::
|
||||||
play-mode = play-current
|
play-mode = play-current
|
||||||
|
|
||||||
[mpv]
|
[mpv]
|
||||||
# Initial video channel. auto selects the default, no disables video.
|
# Options to be parsed to mpv. See OPTIONS section on mpv(1) man pages for
|
||||||
video = auto
|
# its complete list of available options.
|
||||||
# Specify the video output backend to be used. See VIDEO OUTPUT DRIVERS in
|
# For example:
|
||||||
# mpv(1) man page for details and descriptions of available drivers.
|
#vo = xv
|
||||||
video-output =
|
#ontop = yes
|
||||||
|
#border = no
|
||||||
|
#force-window = yes
|
||||||
|
#autofit = 500x280
|
||||||
|
#geometry = -15-50
|
||||||
|
|
||||||
[youtube-dl]
|
[youtube-dl]
|
||||||
# Video format/quality to be passed to youtube-dl. See FORMAT SELECTION in
|
# Video format/quality to be passed to youtube-dl. See FORMAT SELECTION in
|
||||||
# youtube-dl(1) man page for more details and descriptions.
|
# youtube-dl(1) man page for more details and descriptions.
|
||||||
format = best
|
format = bestvideo+bestaudio
|
||||||
|
|
||||||
|
|
||||||
Bugs
|
Bugs
|
||||||
|
|
329
comp
329
comp
|
@ -16,21 +16,22 @@
|
||||||
#
|
#
|
||||||
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
|
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
|
||||||
|
|
||||||
|
__version__ = '0.4.6'
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
from curses.ascii import ctrl, alt
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from gettext import bindtextdomain, gettext as _, textdomain
|
from gettext import bindtextdomain, gettext as _, textdomain
|
||||||
from os import makedirs
|
from os.path import expanduser
|
||||||
from os.path import abspath, dirname, expanduser, expandvars
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
from traceback import print_exception
|
||||||
|
|
||||||
from mpv import MPV
|
from mpv import MPV
|
||||||
from pkg_resources import resource_filename
|
from pkg_resources import resource_filename
|
||||||
from youtube_dl import YoutubeDL
|
|
||||||
|
|
||||||
from omp import extract_info, Omp
|
from omp import extract_info, Omp
|
||||||
|
|
||||||
|
@ -66,19 +67,18 @@ class Comp(Omp):
|
||||||
playing (int): index of playing track in played
|
playing (int): index of playing track in played
|
||||||
playlist (iterator): iterator of tracks according to mode
|
playlist (iterator): iterator of tracks according to mode
|
||||||
reading (bool): flag show if user input is being read
|
reading (bool): flag show if user input is being read
|
||||||
search_res (iterator): title-searched results
|
search_str (str): regex search string
|
||||||
scr (curses WindowObject): curses window object
|
scr (curses WindowObject): curses window object
|
||||||
start (int): index of the first track to be printed on screen
|
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
|
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 = object.__new__(cls)
|
||||||
self.play_backward, self.reading = False, False
|
self.play_backward, self.reading = False, False
|
||||||
self.playing, self.start, self.y = -1, 0, 1
|
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.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,
|
self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
|
||||||
ytdl=True, ytdl_format=ytdlf)
|
ytdl=True, ytdl_format=ytdlf)
|
||||||
self.scr = curses.initscr()
|
self.scr = curses.initscr()
|
||||||
|
@ -90,6 +90,7 @@ class Comp(Omp):
|
||||||
previously on the display.
|
previously on the display.
|
||||||
"""
|
"""
|
||||||
if self.reading: return
|
if self.reading: return
|
||||||
|
curses.update_lines_cols()
|
||||||
y %= curses.LINES
|
y %= curses.LINES
|
||||||
x %= curses.COLS
|
x %= curses.COLS
|
||||||
length = X % curses.COLS - x + (y != curses.LINES - 1)
|
length = X % curses.COLS - x + (y != curses.LINES - 1)
|
||||||
|
@ -103,8 +104,8 @@ class Comp(Omp):
|
||||||
if self.mp.osd.duration is not None:
|
if self.mp.osd.duration is not None:
|
||||||
self.played[self.playing]['duration'] = self.mp.osd.duration
|
self.played[self.playing]['duration'] = self.mp.osd.duration
|
||||||
add_status_str(':', X=5, lpad=3)
|
add_status_str(':', X=5, lpad=3)
|
||||||
if self.vid != 'no': add_status_str('V', x=1, X=2)
|
if self.mp.video: add_status_str('V', x=1, X=2)
|
||||||
if not self.mp.mute: add_status_str('A', X=1)
|
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(self.mp.osd.time_pos or '00:00:00', x=4, X=12)
|
||||||
add_status_str('/', x=13, X=14)
|
add_status_str('/', x=13, X=14)
|
||||||
add_status_str(self.mp.osd.duration or '00:00:00', x=15, X=23)
|
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()
|
self.scr.refresh()
|
||||||
|
|
||||||
def print_msg(self, message, error=False):
|
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
|
attributes = curses.color_pair(1) if error else curses.A_NORMAL
|
||||||
self.adds(message, curses.LINES-1, attr=attributes, lpad=0)
|
self.adds(message, curses.LINES-1, attr=attributes, lpad=0)
|
||||||
self.scr.refresh()
|
self.scr.refresh()
|
||||||
|
@ -122,15 +124,13 @@ class Comp(Omp):
|
||||||
def setno(self, *keys):
|
def setno(self, *keys):
|
||||||
"""Set all keys of each entry in entries to False."""
|
"""Set all keys of each entry in entries to False."""
|
||||||
for entry in self.entries:
|
for entry in self.entries:
|
||||||
for key in keys:
|
for key in keys: entry[key] = False
|
||||||
entry[key] = False
|
|
||||||
|
|
||||||
def play(self, force=False):
|
def play(self, force=False):
|
||||||
"""Play the next track."""
|
"""Play the next track."""
|
||||||
def mpv_play(entry, force):
|
def mpv_play(entry, force):
|
||||||
self.setno('playing')
|
self.setno('playing')
|
||||||
entry['playing'] = True
|
entry['playing'] = True
|
||||||
self.mp.vid = self.vid
|
|
||||||
try:
|
try:
|
||||||
self.mp.play(entry['filename'])
|
self.mp.play(entry['filename'])
|
||||||
except:
|
except:
|
||||||
|
@ -198,7 +198,7 @@ class Comp(Omp):
|
||||||
|
|
||||||
def property_handler(self, name, val): self.update_status()
|
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.noecho()
|
||||||
curses.cbreak()
|
curses.cbreak()
|
||||||
self.scr.keypad(True)
|
self.scr.keypad(True)
|
||||||
|
@ -208,24 +208,11 @@ class Comp(Omp):
|
||||||
for i in range(1, 8): curses.init_pair(i, i, -1)
|
for i in range(1, 8): curses.init_pair(i, i, -1)
|
||||||
curses.init_pair(8, -1, 7)
|
curses.init_pair(8, -1, 7)
|
||||||
for i in range(1, 7): curses.init_pair(i + 8, -1, i)
|
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()
|
self.refresh()
|
||||||
|
|
||||||
def __enter__(self): return self
|
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):
|
def read_input(self, prompt):
|
||||||
"""Print the prompt string at the bottom of the screen then read
|
"""Print the prompt string at the bottom of the screen then read
|
||||||
from standard input.
|
from standard input.
|
||||||
|
@ -267,24 +254,21 @@ class Comp(Omp):
|
||||||
|
|
||||||
def search(self, backward=False):
|
def search(self, backward=False):
|
||||||
"""Prompt then search for a pattern."""
|
"""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 = deque(self.entries)
|
||||||
entries.rotate(-self.idx())
|
if backward:
|
||||||
self.search_res = deque(filter(
|
entries.rotate(-self.idx())
|
||||||
lambda entry: p.search(entry['title']) is not None, entries))
|
entries.reverse()
|
||||||
if backward: self.search_res.reverse()
|
|
||||||
if self.search_res:
|
|
||||||
self.move(self.idx(self.search_res[0]) - self.idx())
|
|
||||||
else:
|
else:
|
||||||
self.print_msg(_("Pattern not found"), error=True)
|
entries.rotate(-self.idx() - 1)
|
||||||
|
for entry in entries:
|
||||||
def next_search(self, backward=False):
|
if pattern.search(entry['title']) is not None:
|
||||||
"""Repeat previous search."""
|
self.move(self.idx(entry) - self.idx())
|
||||||
if self.search_res:
|
return
|
||||||
self.search_res.rotate(1 if backward else -1)
|
self.print_msg(_("'{}' not found").format(self.search_str), error=True)
|
||||||
self.move(self.idx(self.search_res[0]) - self.idx())
|
|
||||||
else:
|
|
||||||
self.print_msg(_("Pattern not found"), error=True)
|
|
||||||
|
|
||||||
def resize(self):
|
def resize(self):
|
||||||
curses.update_lines_cols()
|
curses.update_lines_cols()
|
||||||
|
@ -315,20 +299,19 @@ class Comp(Omp):
|
||||||
curses.echo()
|
curses.echo()
|
||||||
curses.endwin()
|
curses.endwin()
|
||||||
Omp.__exit__(self, exc_type, exc_value, traceback)
|
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 = ArgumentParser(description='Curses Omni Media Player')
|
||||||
parser.add_argument('-v', '--version', action='version',
|
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',
|
parser.add_argument('-e', '--extractor', default='youtube-dl',
|
||||||
choices=('json', 'mpv', 'youtube-dl'), required=False,
|
choices=('json', 'mpv', 'youtube-dl'), required=False,
|
||||||
help='playlist extractor, default is youtube-dl')
|
help='playlist extractor, default is youtube-dl')
|
||||||
parser.add_argument('playlist', help='path or URL to the playlist')
|
parser.add_argument('playlist', help='path or URL to the playlist')
|
||||||
parser.add_argument('-c', '--config', default=USER_CONFIG, required=False,
|
parser.add_argument('-c', '--config', default=USER_CONFIG, required=False,
|
||||||
help='path to the configuration file')
|
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',
|
parser.add_argument('--vo', required=False, metavar='DRIVER',
|
||||||
help='specify the video output backend to be used. See\
|
help='specify the video output backend to be used. See\
|
||||||
VIDEO OUTPUT DRIVERS in mpv(1) for details and\
|
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 ''
|
json_file = args.playlist if args.extractor == 'json' else ''
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
config.read(args.config)
|
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')
|
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:
|
with Comp(entries, json_file, mode, mpv_args, ytdlf) as comp:
|
||||||
c = comp.scr.getch()
|
while True:
|
||||||
while c != 113: # letter q
|
c = comp.scr.get_wch()
|
||||||
if c == 10: # curses.KEY_ENTER doesn't work
|
comp.print_msg('')
|
||||||
comp.update_playlist()
|
# mpv keybindings
|
||||||
comp.next(force=True)
|
if c == curses.KEY_LEFT:
|
||||||
elif c in (32, 112): # space or letter p
|
comp.seek(-5, precision='exact')
|
||||||
comp.mp.pause ^= True
|
elif c == curses.KEY_RIGHT:
|
||||||
elif c == 47: # /
|
comp.seek(5, precision='exact')
|
||||||
comp.search()
|
elif c == curses.KEY_SLEFT: # Shifted Left-arrow
|
||||||
elif c == 60: # <
|
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:
|
try:
|
||||||
if comp.mp.time_pos < 1:
|
if comp.mp.time_pos < 1:
|
||||||
comp.next(backward=True)
|
comp.next(backward=True)
|
||||||
|
@ -366,34 +381,123 @@ with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
|
||||||
comp.seek(0, 'absolute')
|
comp.seek(0, 'absolute')
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
elif c == 62: # >
|
elif c == '>':
|
||||||
comp.next()
|
comp.next()
|
||||||
elif c == 63: # ?
|
elif c == '\n': # curses.KEY_ENTER doesn't work
|
||||||
comp.search(backward=True)
|
comp.update_playlist()
|
||||||
elif c == 65: # letter A
|
comp.next(force=True)
|
||||||
comp.mp.mute ^= True # hack to toggle bool value
|
elif c == 'O':
|
||||||
elif c == 68: # letter D
|
comp.mp.command('cycle-values', 'osd-level', 3, 1)
|
||||||
comp.entries.pop(comp.idx())
|
elif c in ('o', 'P'):
|
||||||
if 1 < len(comp.entries) - curses.LINES + 4 == comp.start:
|
comp.mp.show_progress()
|
||||||
comp.start -= 1
|
elif c == 'z':
|
||||||
elif comp.idx() == len(comp.entries):
|
comp.add('sub-delay', -0.1)
|
||||||
comp.y -= 1
|
elif c == 'x':
|
||||||
comp.refresh()
|
comp.add('sub-delay', 0.1)
|
||||||
elif c == 77: # letter M
|
elif c == ctrl('+'):
|
||||||
comp.mode = MODES[(MODES.index(comp.mode) - 1) % 8]
|
comp.add('audio-delay', 0.1)
|
||||||
comp.update_status()
|
elif c == ctrl('-'):
|
||||||
elif c == 78: # letter N
|
comp.add('audio-delay', -0.1)
|
||||||
comp.next_search(backward=True)
|
elif c in ('/', '9'):
|
||||||
elif c == 86: # letter V
|
comp.add('volume', -2)
|
||||||
comp.vid = 'auto' if comp.vid == 'no' else 'no'
|
elif c in ('*', '0'):
|
||||||
comp.mp.vid = comp.vid
|
comp.add('volume', 2)
|
||||||
comp.update_status()
|
elif c == 'm':
|
||||||
elif c == 87: # letter W
|
comp.cycle('mute')
|
||||||
comp.dump_json()
|
elif c == '1':
|
||||||
elif c == 100: # letter d
|
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.current()['selected'] = not comp.current().get('selected')
|
||||||
comp.move(1)
|
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: "))
|
extractor = comp.read_input(_("Playlist extractor: "))
|
||||||
filename = comp.read_input(_("Insert: "))
|
filename = comp.read_input(_("Insert: "))
|
||||||
entries = extract_info(filename, extractor)
|
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(entries)
|
||||||
comp.entries.extend(bottom)
|
comp.entries.extend(bottom)
|
||||||
comp.refresh()
|
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.mode = MODES[(MODES.index(comp.mode) + 1) % 8]
|
||||||
comp.update_status()
|
comp.update_status()
|
||||||
elif c == 110: # letter n
|
elif c == curses.KEY_DC:
|
||||||
comp.next_search()
|
comp.entries.pop(comp.idx())
|
||||||
elif c == 111: # letter o
|
if 1 < len(comp.entries) - curses.LINES + 4 == comp.start:
|
||||||
extractor = comp.read_input(_("Playlist extractor: "))
|
comp.start -= 1
|
||||||
filename = comp.read_input(_("Open: "))
|
elif comp.idx() == len(comp.entries):
|
||||||
entries = extract_info(filename, extractor)
|
comp.y -= 1
|
||||||
if entries is None:
|
comp.refresh()
|
||||||
comp.print_msg(
|
elif c == 'W':
|
||||||
_("'{}': Can't extract playlist").format(filename))
|
comp.dump_json()
|
||||||
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 in (curses.KEY_F5, curses.KEY_RESIZE):
|
elif c in (curses.KEY_F5, curses.KEY_RESIZE):
|
||||||
comp.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
|
.\" Process this file with
|
||||||
.\" groff -man -Tutf8 comp.1
|
.\" groff -man -Tutf8 comp.1
|
||||||
.\"
|
.\"
|
||||||
.TH COMP 1 2017-06-17 comp
|
.TH COMP 1 2018-01-25 comp
|
||||||
.SH NAME
|
.SH NAME
|
||||||
comp \- Curses Omni Media Player
|
comp \- Curses Omni Media Player
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
@ -11,8 +11,10 @@ comp \- Curses Omni Media Player
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
\fBcomp\fR is a
|
\fBcomp\fR is a
|
||||||
.BR mpv (1)
|
.BR mpv (1)
|
||||||
front-end using curses. It has basic media player functions and can to extract
|
front-end using
|
||||||
playlists from multiple sources such as media sites supported by
|
.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),
|
.BR youtube-dl (1),
|
||||||
local and direct URL to video/audio and its own JSON playlist format.
|
local and direct URL to video/audio and its own JSON playlist format.
|
||||||
.SH OPTIONS
|
.SH OPTIONS
|
||||||
|
@ -47,75 +49,192 @@ for details and descriptions of available drivers
|
||||||
.B -f \fIYTDL_FORMAT\fR, \fB--format \fIYTDL_FORMAT
|
.B -f \fIYTDL_FORMAT\fR, \fB--format \fIYTDL_FORMAT
|
||||||
video format/quality to be passed to youtube-dl
|
video format/quality to be passed to youtube-dl
|
||||||
.SH KEYBOARD CONTROL
|
.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
|
.TP
|
||||||
.B Return
|
.B Return
|
||||||
Start playing.
|
Start playing.
|
||||||
.TP
|
.TP
|
||||||
.B Space, p
|
.B Space / p
|
||||||
Toggle pause.
|
Pause (pressing again unpauses).
|
||||||
.TP
|
.TP
|
||||||
.B /, ?
|
.B .
|
||||||
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.
|
||||||
.TP
|
.TP
|
||||||
.B <, >
|
.B ,
|
||||||
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.
|
||||||
.TP
|
.TP
|
||||||
.B A
|
.B q
|
||||||
Toggle mute.
|
Stop playing and quit.
|
||||||
.TP
|
.TP
|
||||||
.B D
|
.B / and *
|
||||||
Delete the current entry.
|
Decrease/increase volume.
|
||||||
.TP
|
.TP
|
||||||
.B N
|
.B 9 and 0
|
||||||
Repeat previous search in reverse direction.
|
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
|
.TP
|
||||||
.B V
|
.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
|
.TP
|
||||||
.B W
|
.B W
|
||||||
Save the current playlist under JSON format.
|
Save the current playlist under JSON format.
|
||||||
.TP
|
.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
|
.B F5
|
||||||
Redraw the screen content.
|
Redraw the screen content.
|
||||||
|
.TP
|
||||||
|
.B :
|
||||||
|
Execute a mpv command.
|
||||||
.SH FILES
|
.SH FILES
|
||||||
.TP
|
.TP
|
||||||
.I ~/.config/comp/settings.ini
|
.I ~/.config/comp/settings.ini
|
||||||
|
|
Binary file not shown.
|
@ -1,6 +1,6 @@
|
||||||
# SOME DESCRIPTIVE TITLE.
|
# Vietnamese translation for Omp front-ends.
|
||||||
# Copyright (C) YEAR ORGANIZATION
|
# Copyright (C) 2018 Nguyễn Gia Phong
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
# Nguyễn Gia Phong <vn.mcsinyx@gmail.com>, 2018
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -50,6 +50,12 @@ msgstr "Tiêu đề"
|
||||||
msgid "Current size: {}x{}. Minimum size: {}x4."
|
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."
|
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 [{}]: "
|
msgid "Save playlist to [{}]: "
|
||||||
msgstr "Lưu danh sách phát tại [{}]: "
|
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',
|
DEFAULT_ENTRY = {'filename': '', 'title': '', 'duration': '00:00:00',
|
||||||
'error': False, 'playing': False, 'selected': False}
|
'error': False, 'playing': False, 'selected': False}
|
||||||
|
JSON_KEYS = 'filename', 'title', 'duration', 'error', 'selected'
|
||||||
|
|
||||||
|
|
||||||
class YoutubeDLLogger:
|
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>
|
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
|
||||||
|
|
||||||
|
import curses
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
from bisect import bisect_left as bisect
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from gettext import bindtextdomain, gettext as _, textdomain
|
from gettext import bindtextdomain, gettext as _, textdomain
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
from os import makedirs
|
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 random import choice
|
||||||
from time import gmtime, sleep, strftime
|
from sys import exc_info
|
||||||
from urllib import request
|
|
||||||
|
|
||||||
from youtube_dl import YoutubeDL
|
|
||||||
from pkg_resources import resource_filename
|
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
|
# Init gettext
|
||||||
bindtextdomain('omp', resource_filename('omp', 'locale'))
|
bindtextdomain('omp', resource_filename('omp', 'locale'))
|
||||||
textdomain('omp')
|
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):
|
class Omp(object):
|
||||||
"""Omni Media Player meta object.
|
"""Omni Media Player meta object.
|
||||||
|
|
||||||
|
@ -51,7 +61,6 @@ class Omp(object):
|
||||||
playing (int): index of playing track in played
|
playing (int): index of playing track in played
|
||||||
playlist (iterator): iterator of tracks according to mode
|
playlist (iterator): iterator of tracks according to mode
|
||||||
search_res (iterator): title-searched results
|
search_res (iterator): title-searched results
|
||||||
vid (str): flag show if video output is enabled
|
|
||||||
|
|
||||||
I/O handlers (defined by front-end):
|
I/O handlers (defined by front-end):
|
||||||
print_msg(message, error=False): print a message
|
print_msg(message, error=False): print a message
|
||||||
|
@ -59,19 +68,23 @@ class Omp(object):
|
||||||
read_input(prompt): prompt for user input
|
read_input(prompt): prompt for user input
|
||||||
refresh(): update interface content
|
refresh(): update interface content
|
||||||
"""
|
"""
|
||||||
def __new__(cls, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
|
def __new__(cls, entries, json_file, mode, mpv_args, ytdlf):
|
||||||
self = super(Comp, cls).__new__(cls)
|
self = object.__new__(cls)
|
||||||
self.play_backward, self.reading = False, False
|
self.play_backward, self.reading = False, False
|
||||||
self.playing = -1
|
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.entries, self.played = entries, []
|
||||||
self.playlist, self.search_res = iter(()), deque()
|
self.playlist, self.search_res = iter(()), deque()
|
||||||
self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
|
self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
|
||||||
ytdl=True, ytdl_format=ytdlf)
|
ytdl=True, ytdl_format=ytdlf)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
|
def __init__(self, entries, json_file, mode, mpv_args, ytdlf):
|
||||||
if mpv_vo is not None: self.mp['vo'] = mpv_vo
|
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('mute')
|
||||||
@self.mp.property_observer('pause')
|
@self.mp.property_observer('pause')
|
||||||
@self.mp.property_observer('time-pos')
|
@self.mp.property_observer('time-pos')
|
||||||
|
@ -80,43 +93,88 @@ class Omp(object):
|
||||||
|
|
||||||
def __enter__(self): return self
|
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'):
|
def seek(self, amount, reference='relative', precision='default-precise'):
|
||||||
"""Wrap mp.seek with a try clause to avoid crash when nothing is
|
"""Wrap a try clause around mp.seek to avoid crashing when
|
||||||
being played.
|
nothing is being played.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.mp.seek(amount, reference, precision)
|
self.mp.seek(amount, reference, precision)
|
||||||
except:
|
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):
|
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
|
self.play_backward = backward
|
||||||
if self.mp.idle_active:
|
if self.mp.idle_active:
|
||||||
self.play(force)
|
self.play(force)
|
||||||
else:
|
else:
|
||||||
self.seek(100, 'absolute-percent')
|
self.mp.time_pos = self.mp.duration
|
||||||
if force: self.mp.pause = False
|
if force: self.mp.pause = False
|
||||||
|
|
||||||
def search(self, backward=False):
|
def search(self, backward=False):
|
||||||
|
@ -141,19 +199,22 @@ class Omp(object):
|
||||||
self.update_status(_("Pattern not found"), curses.color_pair(1))
|
self.update_status(_("Pattern not found"), curses.color_pair(1))
|
||||||
|
|
||||||
def dump_json(self):
|
def dump_json(self):
|
||||||
|
"""Read user input needed to save the playlist."""
|
||||||
s = self.read_input(
|
s = self.read_input(
|
||||||
_("Save playlist to [{}]: ").format(self.json_file))
|
_("Save playlist to [{}]: ").format(self.json_file))
|
||||||
self.json_file = abspath(expanduser(expandvars(s or 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:
|
try:
|
||||||
makedirs(dirname(self.json_file), exist_ok=True)
|
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:
|
except:
|
||||||
errmsg = _("'{}': Can't open file for writing").format(
|
errmsg = _("'{}': Can't open file for writing").format(
|
||||||
self.json_file)
|
self.json_file)
|
||||||
self.print_msg(errmsg, error=True)
|
self.print_msg(errmsg, error=True)
|
||||||
else:
|
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))
|
self.print_msg(_("'{}' written").format(self.json_file))
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
|
16
settings.ini
16
settings.ini
|
@ -5,13 +5,17 @@
|
||||||
play-mode = play-current
|
play-mode = play-current
|
||||||
|
|
||||||
[mpv]
|
[mpv]
|
||||||
# Initial video channel. auto selects the default, no disables video.
|
# Options to be parsed to mpv. See OPTIONS section on mpv(1) man pages for
|
||||||
video = auto
|
# its complete list of available options.
|
||||||
# Specify the video output backend to be used. See VIDEO OUTPUT DRIVERS in
|
# For example:
|
||||||
# mpv(1) man page for details and descriptions of available drivers.
|
#vo = xv
|
||||||
video-output =
|
#ontop = yes
|
||||||
|
#border = no
|
||||||
|
#force-window = yes
|
||||||
|
#autofit = 500x280
|
||||||
|
#geometry = -15-50
|
||||||
|
|
||||||
[youtube-dl]
|
[youtube-dl]
|
||||||
# Video format/quality to be passed to youtube-dl. See FORMAT SELECTION in
|
# Video format/quality to be passed to youtube-dl. See FORMAT SELECTION in
|
||||||
# youtube-dl(1) man page for more details and descriptions.
|
# 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(
|
setup(
|
||||||
name='comp',
|
name='comp',
|
||||||
version='0.3.12',
|
version='0.4.6',
|
||||||
description=('Curses Omni Media Player'),
|
description=('Curses Omni Media Player'),
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
url='https://github.com/McSinyx/comp',
|
url='https://github.com/McSinyx/comp',
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
"duration": "00:05:21",
|
"duration": "00:05:21",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/weeI1G46q0o",
|
"filename": "https://youtu.be/weeI1G46q0o",
|
||||||
"playing": false,
|
|
||||||
"selected": true,
|
"selected": true,
|
||||||
"title": "DJ Khaled - I'm the One ft. Justin Bieber, Quavo, Chance the Rapper, Lil Wayne"
|
"title": "DJ Khaled - I'm the One ft. Justin Bieber, Quavo, Chance the Rapper, Lil Wayne"
|
||||||
},
|
},
|
||||||
|
@ -11,127 +10,55 @@
|
||||||
"duration": "00:04:23",
|
"duration": "00:04:23",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/JGwWNGJdvx8",
|
"filename": "https://youtu.be/JGwWNGJdvx8",
|
||||||
"playing": false,
|
|
||||||
"selected": true,
|
"selected": true,
|
||||||
"title": "Ed Sheeran - Shape of You [Official Video]"
|
"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",
|
"duration": "00:03:48",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/72UO0v5ESUo",
|
"filename": "https://youtu.be/72UO0v5ESUo",
|
||||||
"playing": false,
|
|
||||||
"selected": true,
|
"selected": true,
|
||||||
"title": "Luis Fonsi, Daddy Yankee - Despacito (Audio) ft. Justin Bieber"
|
"title": "Luis Fonsi, Daddy Yankee - Despacito (Audio) ft. Justin Bieber"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:00:00",
|
"duration": "00:03:41",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/D5drYkLiLI8",
|
"filename": "https://youtu.be/D5drYkLiLI8",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"title": "Kygo, Selena Gomez - It Ain't Me (with Selena Gomez) (Audio)"
|
"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",
|
"duration": "00:12:53",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "test/gplv3.ogg",
|
"filename": "test/gplv3.ogg",
|
||||||
"playing": false,
|
|
||||||
"selected": true,
|
"selected": true,
|
||||||
"title": "gplv3.ogg"
|
"title": "gplv3.ogg"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:00:00",
|
"duration": "00:04:17",
|
||||||
"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",
|
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/dPI-mRFEIH0",
|
"filename": "https://youtu.be/dPI-mRFEIH0",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"title": "Katy Perry - Bon Appétit (Official) ft. Migos"
|
"title": "Katy Perry - Bon Appétit (Official) ft. Migos"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:00:00",
|
"duration": "00:04:06",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/aatr_2MstrI",
|
"filename": "https://youtu.be/aatr_2MstrI",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"title": "Clean Bandit - Symphony feat. Zara Larsson [Official Video]"
|
"title": "Clean Bandit - Symphony feat. Zara Larsson [Official Video]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:34:38",
|
"duration": "00:04:16",
|
||||||
"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",
|
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/7F37r50VUTQ",
|
"filename": "https://youtu.be/7F37r50VUTQ",
|
||||||
"playing": false,
|
|
||||||
"selected": true,
|
"selected": true,
|
||||||
"title": "ZAYN, Taylor Swift - I Don’t Wanna Live Forever (Fifty Shades Darker)"
|
"title": "ZAYN, Taylor Swift - I Don’t Wanna Live Forever (Fifty Shades Darker)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:00:00",
|
"duration": "00:04:57",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/qFLhGq0060w",
|
"filename": "https://youtu.be/qFLhGq0060w",
|
||||||
"playing": false,
|
|
||||||
"selected": true,
|
"selected": true,
|
||||||
"title": "The Weeknd - I Feel It Coming ft. Daft Punk"
|
"title": "The Weeknd - I Feel It Coming ft. Daft Punk"
|
||||||
},
|
},
|
||||||
|
@ -139,55 +66,41 @@
|
||||||
"duration": "00:02:29",
|
"duration": "00:02:29",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "http://www.html5videoplayer.net/videos/toystory.mp4",
|
"filename": "http://www.html5videoplayer.net/videos/toystory.mp4",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"title": "toystory.mp4"
|
"title": "toystory.mp4"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:00:00",
|
"duration": "00:05:13",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/6ImFf__U6io",
|
"filename": "https://youtu.be/6ImFf__U6io",
|
||||||
"playing": false,
|
|
||||||
"selected": true,
|
"selected": true,
|
||||||
"title": "Birdman - Dark Shades (Explicit) ft. Lil Wayne, Mack Maine"
|
"title": "Birdman - Dark Shades (Explicit) ft. Lil Wayne, Mack Maine"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:03:56",
|
"duration": "00:03:55",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://www.youtube.com/watch?v=3M3xfu0m5o4",
|
"filename": "https://www.youtube.com/watch?v=3M3xfu0m5o4",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"title": "David Banner - Play (Dirty version)"
|
"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",
|
"duration": "00:03:55",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/NGLxoKOvzu4",
|
"filename": "https://youtu.be/NGLxoKOvzu4",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"title": "Jason Derulo - Swalla (feat. Nicki Minaj & Ty Dolla $ign) (Official Music Video)"
|
"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",
|
"duration": "00:03:51",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/nfs8NYg7yQM",
|
"filename": "https://youtu.be/nfs8NYg7yQM",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"title": "Charlie Puth - Attention [Official Video]"
|
"title": "Charlie Puth - Attention [Official Video]"
|
||||||
},
|
},
|
||||||
|
@ -195,39 +108,27 @@
|
||||||
"duration": "00:04:10",
|
"duration": "00:04:10",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://www.youtube.com/watch?v=sRIkXM8S1J8",
|
"filename": "https://www.youtube.com/watch?v=sRIkXM8S1J8",
|
||||||
"playing": false,
|
|
||||||
"selected": true,
|
"selected": true,
|
||||||
"title": "Best Goat Song Versions Compilation Ever! (HD)"
|
"title": "Best Goat Song Versions Compilation Ever! (HD)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:00:00",
|
"duration": "00:04:03",
|
||||||
"error": false,
|
|
||||||
"filename": "https://youtu.be/Dst9gZkq1a8",
|
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
|
||||||
"title": "Travis Scott - goosebumps ft. Kendrick Lamar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"duration": "00:00:00",
|
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/dMK_npDG12Q",
|
"filename": "https://youtu.be/dMK_npDG12Q",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"title": "Lorde - Green Light"
|
"title": "Lorde - Green Light"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:00:00",
|
"duration": "00:03:32",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/h--P8HzYZ74",
|
"filename": "https://youtu.be/h--P8HzYZ74",
|
||||||
"playing": false,
|
|
||||||
"selected": true,
|
"selected": true,
|
||||||
"title": "Zedd, Alessia Cara - Stay (Lyric Video)"
|
"title": "Zedd, Alessia Cara - Stay (Lyric Video)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:00:00",
|
"duration": "00:02:45",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/Mdh2p03cRfw",
|
"filename": "https://youtu.be/Mdh2p03cRfw",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"title": "Sam Hunt - Body Like A Back Road (Audio)"
|
"title": "Sam Hunt - Body Like A Back Road (Audio)"
|
||||||
},
|
},
|
||||||
|
@ -235,7 +136,6 @@
|
||||||
"duration": "00:03:40",
|
"duration": "00:03:40",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/Fq0xEpRDL9Q",
|
"filename": "https://youtu.be/Fq0xEpRDL9Q",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"title": "Chris Brown - Privacy (Explicit Version)"
|
"title": "Chris Brown - Privacy (Explicit Version)"
|
||||||
},
|
},
|
||||||
|
@ -243,87 +143,55 @@
|
||||||
"duration": "00:03:36",
|
"duration": "00:03:36",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/7wtfhZwyrcc",
|
"filename": "https://youtu.be/7wtfhZwyrcc",
|
||||||
"playing": true,
|
|
||||||
"selected": false,
|
"selected": false,
|
||||||
"title": "Imagine Dragons - Believer"
|
"title": "Imagine Dragons - Believer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:00:00",
|
"duration": "00:03:52",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/t_jHrUE5IOk",
|
"filename": "https://youtu.be/A-Rn0iQEpc8",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"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",
|
"duration": "00:00:00",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/wzZWXrlDj-A",
|
"filename": "https://youtu.be/VNkDJk5_9eU",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"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,
|
"error": false,
|
||||||
"filename": "https://youtu.be/AEB6ibtdPZc",
|
"filename": "https://youtu.be/M9bq_alk-sw",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"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,
|
"error": false,
|
||||||
"filename": "https://youtu.be/vqW18C4plZ8",
|
"filename": "https://youtu.be/oHNKTlz1lps",
|
||||||
"playing": false,
|
"selected": true,
|
||||||
"selected": false,
|
"title": "Linus Said - Music Parody (Momma Said)"
|
||||||
"title": "WizKid - Come Closer ft. Drake"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"duration": "00:00:00",
|
"duration": "00:03:58",
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/A7xzXDStQnk",
|
"filename": "https://youtu.be/4VrhlyIgo3M",
|
||||||
"playing": false,
|
|
||||||
"selected": false,
|
"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",
|
"duration": "00:03:52",
|
||||||
"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",
|
|
||||||
"error": false,
|
"error": false,
|
||||||
"filename": "https://youtu.be/9sg-A-eS6Ig",
|
"filename": "https://youtu.be/9sg-A-eS6Ig",
|
||||||
"playing": false,
|
|
||||||
"selected": true,
|
"selected": true,
|
||||||
"title": "Enrique Iglesias - SUBEME LA RADIO (Official Video) ft. Descemer Bueno, Zion & Lennox"
|
"title": "Enrique Iglesias - SUBEME LA RADIO (Official Video) ft. Descemer Bueno, Zion & Lennox"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue