Compare commits

..

47 commits

Author SHA1 Message Date
e860f1526d Remove Travis CI
Ubuntu Trusty doesn't provide packages needed for testing, namely libmpv1
and Python 3.7+.
2018-09-19 17:19:47 +07:00
6576c01801 Clean up and update Travis intergration 2018-09-18 20:57:14 +07:00
cclauss
900a3f359a Use Travis CI to run Continuous Integration tests 2018-04-22 09:53:56 +07:00
7203eaaef5 Remove undefined names in omp/omp.py 2018-04-22 09:53:56 +07:00
cclauss
51ba038b80 import curses, re in omp/omp.py 2018-04-22 09:53:21 +07:00
be0eaeadb7 Give more sense to shuffle-* and *-selected modes (#8) 2018-01-30 17:55:27 +07:00
f8185d1b31 Make comp ask to dump when quit and dump less bullsh*t 2018-01-30 17:08:47 +07:00
082685cee3 Fix KeyError when there isn't mpv section in config file (#7) 2018-01-30 09:40:22 +07:00
0500d98b0a Add support for mpv arguments in config file (#6) 2018-01-28 23:12:13 +07:00
bb29631789 Update keybindings documentation 2018-01-25 22:09:23 +07:00
f4791e6e99 Add warning messages 2018-01-25 22:09:23 +07:00
37f92bcc74 Fix duplicated keybindings 2018-01-25 22:09:23 +07:00
e21fd6285d Make compatible with python-mpv 0.3.5 and edit search keybindings 2018-01-25 22:09:23 +07:00
4b18b5c1bf Start implementing default mpv keybindings 2018-01-25 22:09:23 +07:00
6ada63f856 Clean up 2018-01-25 22:09:22 +07:00
b01b6abd1a Make compatible with python-mpv 0.3 2018-01-25 22:09:22 +07:00
7542fb3892 Change mpv and youtube-dl links 2018-01-25 22:09:22 +07:00
1b0692417f Update documentation and drop gettext support for argparse help strings 2018-01-25 22:09:22 +07:00
ba3a065006 Fix JSON playlist dumper and edit keybindings 2018-01-25 22:09:22 +07:00
04901f33d9 Update documentation 2018-01-25 22:09:22 +07:00
9cef1e2382 Fix class calling 2018-01-25 22:09:22 +07:00
2972111b59 Fix youtube-dl stderr garbage printing and edit installation installation instruction 2018-01-25 22:09:22 +07:00
6d0aa7fe51 Update documentations and clean up (?) 2018-01-25 22:09:22 +07:00
801c439146 Update Vietnamese translation 2018-01-25 22:09:22 +07:00
031f9ea1aa Improve info extractor 2018-01-25 22:09:22 +07:00
654c5572ef Improve printing (slightly better performance) 2018-01-25 22:09:22 +07:00
9129a8974f Clean up everything 2018-01-25 22:09:21 +07:00
98c73ae8ac Code clean-up 2018-01-25 22:09:21 +07:00
0bca71fe0c Begin to create an independant back-end object 2018-01-25 22:09:21 +07:00
8334263141 Sync with jaseg/python-mpv 2018-01-25 22:09:21 +07:00
056727768d Add search function 2018-01-25 22:09:21 +07:00
d146f5d74c Refine codebase 2018-01-25 22:09:21 +07:00
df98e90366 Refactor partially (now more things are broken) 2018-01-25 22:09:21 +07:00
cf9cfd3c01 Create an object to avoid using global variable 2018-01-25 22:09:21 +07:00
fc6b4a0c51 Fix the bug when the playlist is shorter than the screen 2018-01-25 22:09:21 +07:00
c1b0652078 Import youtube-dl 2018-01-25 22:09:21 +07:00
6a2c20e490 Add save playlist function and complete Vietnamese translation 2018-01-25 22:09:21 +07:00
3679965578 Add Youtube playlist argument, seek function and patial support for translation using gettext 2018-01-25 22:09:21 +07:00
23fc9ae49b Update documentation and setup 2018-01-25 22:09:21 +07:00
5abe0b2ebc Add repeat and shuffle mode 2018-01-25 22:09:21 +07:00
3f603ed693 Add playing title to statusline and audio, video toggle feature 2018-01-25 22:09:20 +07:00
d5117c65c8 Add pause function and time-pos 2018-01-25 22:09:20 +07:00
1b130724d2 Standardize function calls 2018-01-25 22:09:20 +07:00
c9fcb30f75 Refactor to not use curses.wrapper 2018-01-25 22:09:20 +07:00
1e1418f8dd Add bottom panel and improve data structure 2018-01-25 22:09:20 +07:00
7066eeb697 Initial comp commit 2018-01-25 22:09:20 +07:00
jaseg
f8b6ac8f66 Rebase python-mpv commits
Now with mpv.py moved out of the repository, comp isn't any longer a
fork of python-mpv. These commits confuses the log and thus got squashed
into one.
2018-01-25 22:06:26 +07:00
10 changed files with 727 additions and 455 deletions

View file

@ -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
View file

@ -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)

View file

@ -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.

View file

@ -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 [{}]: "

View file

@ -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:

View file

@ -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):

View file

@ -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

View file

@ -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',

View file

@ -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 - Thats 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 Dont Wanna Live Forever (Fifty Shades Darker)" "title": "ZAYN, Taylor Swift - I Dont 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"
} }