Clean up everything
This commit is contained in:
parent
98c73ae8ac
commit
9129a8974f
|
@ -1,2 +1 @@
|
||||||
# Include the license file
|
include LICENSE README.rst doc/screenshot.png
|
||||||
include LICENSE README.rst
|
|
||||||
|
|
47
README.rst
47
README.rst
|
@ -1,36 +1,39 @@
|
||||||
=================================
|
===============================
|
||||||
comp - Curses Online Media Player
|
comp - Curses Omni Media Player
|
||||||
=================================
|
===============================
|
||||||
|
|
||||||
This program is a curses front-end for mpv and youtube-dl.
|
This program is a curses front-end for mpv and youtube-dl.
|
||||||
|
|
||||||
.. image:: https://ipfs.io/ipfs/QmVhz4F53Sym48kXC7vhDMFsfvJ7iL8gaQ1EgoQADJvuAB
|
.. image:: doc/screenshot.png
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Dependencies
|
comp requires Python 3.5+ with ``curses`` module (only available on Unix-like
|
||||||
^^^^^^^^^^^^
|
OSes such as GNU/Linux and the BSDs) and ``libmpv``. It also depends on
|
||||||
|
``python-mpv`` and ``youtube-dl`` but the setup program will automatically
|
||||||
|
install them if they are missing.
|
||||||
|
|
||||||
This program currently only runs on Python 3.5+ on operating systems that the
|
Using pip
|
||||||
``curses`` module is supported (i.e. Unix-like OS, e.g. GNU/Linux, macOS and
|
^^^^^^^^^
|
||||||
the BSDs).
|
|
||||||
|
|
||||||
It also depends on ``youtube-dl`` and ``libmpv``. Both of those should be
|
Python 2 is still the default on most distributions so the command would be
|
||||||
available in your operating system's repository, although it's more
|
``pip3 install comp``. You can use the ``--user`` flag to avoid system-wide
|
||||||
recommended to install ``youtube-dl`` using ``pip`` (currently most distros
|
installation.
|
||||||
still use Python 2 as default so the command is something like ``pip3 install
|
|
||||||
youtube-dl``).
|
|
||||||
|
|
||||||
Installing comp
|
Using setup.py
|
||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
I will try to upload the program to PyPI when it's more completed but as of
|
To install the latest version or test the development branch (called
|
||||||
this moment, I'd suggest you to use ``git`` to get the software::
|
``bachelor``, in contrast to ``master``), you'll need to do it manually::
|
||||||
|
|
||||||
git clone https://github.com/McSinyx/comp.git
|
git clone https://github.com/McSinyx/comp.git
|
||||||
cd comp
|
cd comp
|
||||||
sudo ./setup.py install
|
git checkout bachelor # usually master is synced with the PyPI repo
|
||||||
|
sudo ./setup.py install -e .
|
||||||
|
|
||||||
|
Note ``setup.py`` uses ``setuptools`` which is a third-party module and can be
|
||||||
|
install using ``pip3``.
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
@ -88,6 +91,8 @@ Keyboard control
|
||||||
+--------------+---------------------------------------------+
|
+--------------+---------------------------------------------+
|
||||||
| ``d`` | Delete current entry |
|
| ``d`` | Delete current entry |
|
||||||
+--------------+---------------------------------------------+
|
+--------------+---------------------------------------------+
|
||||||
|
| ``i`` | Insert playlist |
|
||||||
|
+--------------+---------------------------------------------+
|
||||||
| ``m``, ``M`` | Cycle through playing modes |
|
| ``m``, ``M`` | Cycle through playing modes |
|
||||||
+--------------+---------------------------------------------+
|
+--------------+---------------------------------------------+
|
||||||
| ``n`` | Repeat previous search |
|
| ``n`` | Repeat previous search |
|
||||||
|
@ -120,8 +125,8 @@ Keyboard control
|
||||||
Configuration files
|
Configuration files
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
The system-wide configuration file is ``/etc/comp/settings.ini``, the
|
If not specified by the ``--config``, (user-specific) configuration file is
|
||||||
user-specific one is ``~/.config/mpv/settings.ini``. Default configurations
|
``~/.config/mpv/settings.ini``. Default configurations
|
||||||
are listed below::
|
are listed below::
|
||||||
|
|
||||||
[comp]
|
[comp]
|
||||||
|
|
87
comp
87
comp
|
@ -1,6 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
#
|
# comp - Curses Omni Media Player
|
||||||
# comp - Curses Online Media Player
|
|
||||||
#
|
#
|
||||||
# comp is free software: you can redistribute it and/or modify
|
# comp is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Affero General Public License as
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
@ -24,18 +23,20 @@ from argparse import ArgumentParser
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from gettext import gettext as _, textdomain
|
from gettext import bindtextdomain, gettext as _, textdomain
|
||||||
from os import makedirs
|
from os import makedirs
|
||||||
from os.path import abspath, dirname, expanduser, isfile
|
from os.path import abspath, dirname, expanduser, isfile
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from youtube_dl import YoutubeDL
|
|
||||||
from mpv import MPV
|
from mpv import MPV
|
||||||
|
from pkg_resources import resource_filename
|
||||||
|
from youtube_dl import YoutubeDL
|
||||||
|
|
||||||
from omp import extract_info, Omp
|
from omp import extract_info, Omp
|
||||||
|
|
||||||
# Init gettext
|
# Init gettext
|
||||||
textdomain('comp')
|
bindtextdomain('omp', resource_filename('omp', 'locale'))
|
||||||
|
textdomain('omp')
|
||||||
|
|
||||||
# Global constants
|
# Global constants
|
||||||
SYSTEM_CONFIG = '/etc/comp/settings.ini'
|
SYSTEM_CONFIG = '/etc/comp/settings.ini'
|
||||||
|
@ -92,6 +93,8 @@ class Comp(Omp):
|
||||||
' ' if self.vid == 'no' else 'V')
|
' ' if self.vid == 'no' else 'V')
|
||||||
adds(right.rjust(curses.COLS), curses.color_pair(12))
|
adds(right.rjust(curses.COLS), curses.color_pair(12))
|
||||||
try:
|
try:
|
||||||
|
self.played[self.playing]['duration'] = self.mp.osd.duration
|
||||||
|
self.print(self.played[self.playing])
|
||||||
left = ' {} / {} {} '.format(
|
left = ' {} / {} {} '.format(
|
||||||
self.mp.osd.time_pos, self.mp.osd.duration,
|
self.mp.osd.time_pos, self.mp.osd.duration,
|
||||||
'|' if self.mp.pause else '>')
|
'|' if self.mp.pause else '>')
|
||||||
|
@ -151,7 +154,7 @@ class Comp(Omp):
|
||||||
def _writeln(self, y, title, duration, attr):
|
def _writeln(self, y, title, duration, attr):
|
||||||
title_len = curses.COLS-DURATION_COL_LEN-3
|
title_len = curses.COLS-DURATION_COL_LEN-3
|
||||||
title = justified(title, title_len)
|
title = justified(title, title_len)
|
||||||
duration = duration.ljust(DURATION_COL_LEN)
|
duration = (duration or '00:00:00').ljust(DURATION_COL_LEN)
|
||||||
self.scr.addstr(y, 0, ' {} {} '.format(title, duration), attr)
|
self.scr.addstr(y, 0, ' {} {} '.format(title, duration), attr)
|
||||||
self.scr.refresh()
|
self.scr.refresh()
|
||||||
|
|
||||||
|
@ -188,7 +191,7 @@ class Comp(Omp):
|
||||||
|
|
||||||
def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
|
def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
|
||||||
Omp.__init__(self, entries, lambda name, val: self.update_status(),
|
Omp.__init__(self, entries, lambda name, val: self.update_status(),
|
||||||
json_file, mode, mpv_vo, mpv_vid, ytdlf)
|
json_file, mode, mpv_vid, mpv_vo, ytdlf)
|
||||||
curses.noecho()
|
curses.noecho()
|
||||||
curses.cbreak()
|
curses.cbreak()
|
||||||
self.scr.keypad(True)
|
self.scr.keypad(True)
|
||||||
|
@ -319,7 +322,7 @@ 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('file', help=_("path or URL to the playlist to be opened"))
|
parser.add_argument('file', help=_("path or URL to the playlist to be opened"))
|
||||||
parser.add_argument('-c', '--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,
|
parser.add_argument('--vid', required=False,
|
||||||
help=_("initial video channel. auto selects the default,\
|
help=_("initial video channel. auto selects the default,\
|
||||||
|
@ -333,32 +336,12 @@ parser.add_argument('-f', '--format', required=False, metavar='YTDL_FORMAT',
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
entries = extract_info(args.file, args.extractor)
|
entries = extract_info(args.file, args.extractor)
|
||||||
json_file = args.file if args.extractor == 'json' else ''
|
json_file = args.file if args.extractor == 'json' else ''
|
||||||
|
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
if args.config is not None and isfile(args.config):
|
config.read(args.config)
|
||||||
config_file = args.config
|
vid = args.vid or config.get('mpv', 'video', fallback='auto')
|
||||||
elif isfile(USER_CONFIG):
|
vo = args.vo or config.get('mpv', 'video-output', fallback=None)
|
||||||
config_file = USER_CONFIG
|
|
||||||
else:
|
|
||||||
config_file = SYSTEM_CONFIG
|
|
||||||
config.read(config_file)
|
|
||||||
|
|
||||||
if args.vid is not None:
|
|
||||||
vid = args.vid
|
|
||||||
else:
|
|
||||||
vid = config.get('mpv', 'video', fallback='auto')
|
|
||||||
|
|
||||||
if args.vo is not None:
|
|
||||||
vo = args.vo
|
|
||||||
else:
|
|
||||||
vo = 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')
|
||||||
if args.format is not None:
|
|
||||||
ytdlf = args.format
|
|
||||||
else:
|
|
||||||
ytdlf = config.get('youtube-dl', 'format', fallback='best')
|
|
||||||
|
|
||||||
with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
|
with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
|
||||||
c = comp.scr.getch()
|
c = comp.scr.getch()
|
||||||
|
@ -399,7 +382,9 @@ with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
|
||||||
if s: comp.json_file = s
|
if s: comp.json_file = s
|
||||||
try:
|
try:
|
||||||
makedirs(dirname(abspath(comp.json_file)), exist_ok=True)
|
makedirs(dirname(abspath(comp.json_file)), exist_ok=True)
|
||||||
with open(comp.json_file, 'w') as f: json.dump(comp.entries, f)
|
with open(comp.json_file, 'w') as f:
|
||||||
|
json.dump(comp.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(
|
||||||
comp.json_file)
|
comp.json_file)
|
||||||
|
@ -407,14 +392,25 @@ with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
|
||||||
else:
|
else:
|
||||||
comp.update_status(_("'{}' written").format(comp.json_file))
|
comp.update_status(_("'{}' written").format(comp.json_file))
|
||||||
elif c == 100: # letter d
|
elif c == 100: # letter d
|
||||||
i = comp.idx()
|
comp.entries.pop(comp.idx())
|
||||||
if i + 1 < len(entries):
|
if 1 < len(comp.entries) - curses.LINES + 4 == comp.start:
|
||||||
comp.entries.pop(i)
|
comp.start -= 1
|
||||||
elif len(entries) > 1:
|
elif comp.idx() == len(comp.entries):
|
||||||
comp.entries.pop(i)
|
comp.y -= 1
|
||||||
else:
|
|
||||||
comp.entries = []
|
|
||||||
comp.redraw()
|
comp.redraw()
|
||||||
|
elif c == 105: # letter i
|
||||||
|
extractor = comp.gets(_("Playlist extractor: "))
|
||||||
|
filename = comp.gets(_("Insert: "))
|
||||||
|
entries = extract_info(filename, extractor)
|
||||||
|
if entries is None:
|
||||||
|
comp.update_status(
|
||||||
|
_("'{}': Can't extract playlist").format(filename))
|
||||||
|
else:
|
||||||
|
bottom = comp.entries[comp.idx():]
|
||||||
|
comp.entries = comp.entries[:comp.idx()]
|
||||||
|
comp.entries.extend(entries)
|
||||||
|
comp.entries.extend(bottom)
|
||||||
|
comp.redraw()
|
||||||
elif c == 109: # letter m
|
elif c == 109: # letter 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()
|
||||||
|
@ -422,9 +418,14 @@ with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
|
||||||
comp.next_search()
|
comp.next_search()
|
||||||
elif c == 111: # letter o
|
elif c == 111: # letter o
|
||||||
extractor = comp.gets(_("Playlist extractor: "))
|
extractor = comp.gets(_("Playlist extractor: "))
|
||||||
comp.entries = extract_info(comp.gets(_("Open: ")), extractor)
|
filename = comp.gets(_("Open: "))
|
||||||
comp.start, comp.y = 0, 1
|
entries = extract_info(filename, extractor)
|
||||||
comp.redraw()
|
if entries is None:
|
||||||
|
comp.update_status(
|
||||||
|
_("'{}': Can't extract playlist").format(filename))
|
||||||
|
else:
|
||||||
|
comp.entries, comp.start, comp.y = entries, 0, 1
|
||||||
|
comp.redraw()
|
||||||
elif c == 112: # letter p
|
elif c == 112: # letter p
|
||||||
comp.mp.pause ^= True
|
comp.mp.pause ^= True
|
||||||
elif c in (curses.KEY_UP, 107): # up arrow or letter k
|
elif c in (curses.KEY_UP, 107): # up arrow or letter k
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 811 KiB |
|
@ -0,0 +1,24 @@
|
||||||
|
# omp - Omni Media Player
|
||||||
|
# This is a part of comp
|
||||||
|
#
|
||||||
|
# comp is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# comp program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with comp. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
|
||||||
|
|
||||||
|
"""Omni Media Player - an handy mpv front-end library for interactive
|
||||||
|
control.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .ie import extract_info
|
||||||
|
from .omp import Omp
|
|
@ -0,0 +1,114 @@
|
||||||
|
# ie.py - Omni Media Player infomation extractor
|
||||||
|
# This is a part of comp
|
||||||
|
#
|
||||||
|
# comp is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# comp program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with comp. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
|
||||||
|
|
||||||
|
import json
|
||||||
|
from os.path import abspath, expanduser, expandvars, isfile
|
||||||
|
from time import gmtime, sleep, strftime
|
||||||
|
|
||||||
|
from youtube_dl import YoutubeDL
|
||||||
|
from mpv import MPV
|
||||||
|
|
||||||
|
DEFAULT_ENTRY = {'filename': '', 'title': '', 'duration': '00:00:00',
|
||||||
|
'error': False, 'playing': False, 'selected': False}
|
||||||
|
YTDL_OPTS = {'quiet': True, 'default_search': 'ytsearch',
|
||||||
|
'extract_flat': 'in_playlist'}
|
||||||
|
|
||||||
|
|
||||||
|
def json_extract_info(filename):
|
||||||
|
"""Return list of entries extracted from a file using json. If an
|
||||||
|
error occur during the extraction, return None.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(filename) as f: raw_info, info = json.load(f), []
|
||||||
|
for i in raw_info:
|
||||||
|
e = DEFAULT_ENTRY.copy()
|
||||||
|
for k in e:
|
||||||
|
if k in i and isinstance(i[k], type(e[k])): e[k] = i[k]
|
||||||
|
info.append(e)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def mpv_extract_info(filename):
|
||||||
|
"""Return list of entries extracted from a path or URL using mpv. If
|
||||||
|
an error occur during the extraction, return None.
|
||||||
|
"""
|
||||||
|
mp = MPV(ytdl=True, vid=False)
|
||||||
|
mp.play(filename)
|
||||||
|
while mp.duration is None:
|
||||||
|
sleep(0.25)
|
||||||
|
if mp.playback_abort: return None
|
||||||
|
info = {'filename': filename, 'title': mp.media_title.decode(),
|
||||||
|
'duration': mp.osd.duration, 'error': False, 'playing': False,
|
||||||
|
'selected': False}
|
||||||
|
mp.quit()
|
||||||
|
return [info]
|
||||||
|
|
||||||
|
|
||||||
|
def ytdl_extract_info(filename):
|
||||||
|
"""Return list of entries extracted from a path or URL using
|
||||||
|
youtube-dl. If an error occur during the extraction, return None.
|
||||||
|
"""
|
||||||
|
with YoutubeDL(YTDL_OPTS) as ytdl:
|
||||||
|
try:
|
||||||
|
raw_info = ytdl.extract_info(filename, download=False)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
info = raw_info.get('entries', [raw_info])
|
||||||
|
for i in info:
|
||||||
|
if 'webpage_url' in i:
|
||||||
|
i['filename'] = i['webpage_url']
|
||||||
|
elif (i['ie_key'] == 'Youtube'
|
||||||
|
or i['extractor'] == 'youtube'):
|
||||||
|
i['filename'] = 'https://youtu.be/' + i['id']
|
||||||
|
else:
|
||||||
|
i['filename'] = i['url']
|
||||||
|
if 'title' not in i:
|
||||||
|
try:
|
||||||
|
i['title'] = ytdl.extract_info(i['filename'],
|
||||||
|
download=False)['title']
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
if 'duration' not in i:
|
||||||
|
i['duration'] = '00:00:00'
|
||||||
|
elif isinstance(i['duration'], int):
|
||||||
|
i['duration'] = strftime('%H:%M:%S', gmtime(i['duration']))
|
||||||
|
for k in 'error', 'playing', 'selected': i.setdefault(k, False)
|
||||||
|
for k in i.copy():
|
||||||
|
if k not in DEFAULT_ENTRY: i.pop(k)
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def extract_info(filename, extractor='youtube-dl'):
|
||||||
|
"""Return list of entries extracted from a path or URL using
|
||||||
|
specified extractor. If an error occur during the extraction,
|
||||||
|
return None.
|
||||||
|
|
||||||
|
The extractor could be either 'json', 'mpv' or 'youtube-dl' and
|
||||||
|
fallback to 'youtube-dl'.
|
||||||
|
"""
|
||||||
|
if isfile(expanduser(expandvars(filename))):
|
||||||
|
filename = abspath(expanduser(expandvars(filename)))
|
||||||
|
if extractor == 'json':
|
||||||
|
return json_extract_info(filename)
|
||||||
|
elif extractor == 'mpv':
|
||||||
|
return mpv_extract_info(filename)
|
||||||
|
else:
|
||||||
|
return ytdl_extract_info(filename)
|
Binary file not shown.
|
@ -0,0 +1,72 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR ORGANIZATION
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: \n"
|
||||||
|
"POT-Creation-Date: 2017-04-05 11:00+0700\n"
|
||||||
|
"PO-Revision-Date: 2017-04-06 22:29+0700\n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Generated-By: pygettext.py 1.5\n"
|
||||||
|
"X-Generator: Poedit 1.8.11\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
"Language: vi_VN\n"
|
||||||
|
|
||||||
|
msgid "Curses Online Media Player"
|
||||||
|
msgstr "Phần mềm chơi đa phương tiện trực tuyến sử dụng curses"
|
||||||
|
|
||||||
|
msgid "play-current"
|
||||||
|
msgstr "chơi-một"
|
||||||
|
|
||||||
|
msgid "play-all"
|
||||||
|
msgstr "chơi-tất-cả"
|
||||||
|
|
||||||
|
msgid "play-selected"
|
||||||
|
msgstr "chơi-đã-chọn"
|
||||||
|
|
||||||
|
msgid "repeat-current"
|
||||||
|
msgstr "lặp-một"
|
||||||
|
|
||||||
|
msgid "repeat-all"
|
||||||
|
msgstr "lặp-tất-cả"
|
||||||
|
|
||||||
|
msgid "repeat-selected"
|
||||||
|
msgstr "lặp-đã-chọn"
|
||||||
|
|
||||||
|
msgid "shuffle-all"
|
||||||
|
msgstr "ngẫu-nhiên-tất-cả"
|
||||||
|
|
||||||
|
msgid "shuffle-selected"
|
||||||
|
msgstr "ngẫu-nhiên-đã-chọn"
|
||||||
|
|
||||||
|
msgid "URL"
|
||||||
|
msgstr "URL"
|
||||||
|
|
||||||
|
msgid "Title"
|
||||||
|
msgstr "Tiêu đề"
|
||||||
|
|
||||||
|
msgid "Source"
|
||||||
|
msgstr "Nguồn"
|
||||||
|
|
||||||
|
msgid "Current size: {}x{}. Minimum size: {}x4."
|
||||||
|
msgstr "Kích thước hiện tại: {}x{}. Kích thước tối thiểu: {}x4."
|
||||||
|
|
||||||
|
msgid "Save playlist to [{}]:"
|
||||||
|
msgstr "Lưu playlist tại [{}]:"
|
||||||
|
|
||||||
|
msgid "'{}': Can't open file for writing"
|
||||||
|
msgstr "'{}': Không mở được tệp để ghi"
|
||||||
|
|
||||||
|
msgid "'{}' written"
|
||||||
|
msgstr "'{}' đã ghi"
|
||||||
|
|
||||||
|
msgid "path to playlist in JSON format"
|
||||||
|
msgstr "đường dẫn đến playlist ở định dạng JSON"
|
||||||
|
|
||||||
|
msgid "URL to an playlist on Youtube"
|
||||||
|
msgstr "URL của playlist trên Youtube"
|
|
@ -1,4 +1,4 @@
|
||||||
# omp.py - comp library for playing and playlist management
|
# omp.py - Omni Media Player meta object
|
||||||
# This is a part of comp
|
# This is a part of comp
|
||||||
#
|
#
|
||||||
# comp is free software: you can redistribute it and/or modify
|
# comp is free software: you can redistribute it and/or modify
|
||||||
|
@ -10,7 +10,6 @@
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU Affero General Public License for more details.
|
# GNU Affero General Public License for more details.
|
||||||
|
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with comp. If not, see <http://www.gnu.org/licenses/>.
|
# along with comp. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
@ -22,8 +21,8 @@ from collections import deque
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
from os.path import abspath, expanduser, expandvars, isfile
|
from os.path import abspath, expanduser, expandvars, isfile
|
||||||
from random import choice
|
from random import choice
|
||||||
from requests import head
|
|
||||||
from time import gmtime, sleep, strftime
|
from time import gmtime, sleep, strftime
|
||||||
|
from urllib import request
|
||||||
|
|
||||||
from youtube_dl import YoutubeDL
|
from youtube_dl import YoutubeDL
|
||||||
from mpv import MPV, MpvFormat
|
from mpv import MPV, MpvFormat
|
||||||
|
@ -34,79 +33,8 @@ YTDL_OPTS = {'quiet': True, 'default_search': 'ytsearch',
|
||||||
'extract_flat': 'in_playlist'}
|
'extract_flat': 'in_playlist'}
|
||||||
|
|
||||||
|
|
||||||
def extract_info(filename, extractor='youtube-dl'):
|
|
||||||
"""Return list of entries extracted from a path or URL using
|
|
||||||
specified extractor.
|
|
||||||
|
|
||||||
The extractor could be either 'json', 'mpv' or 'youtube-dl'. If it
|
|
||||||
is not one of them or not specified, youtube-dl will be used.
|
|
||||||
"""
|
|
||||||
def json_extract_info(filename):
|
|
||||||
try:
|
|
||||||
with open(filename) as f: raw_info = json.load(f)
|
|
||||||
info = []
|
|
||||||
for i in raw_info:
|
|
||||||
e = DEFAULT_ENTRY.copy()
|
|
||||||
for k in e:
|
|
||||||
if k in i and isinstance(i[k], type(e[k])): e[k] = i[k]
|
|
||||||
info.append(e)
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
else:
|
|
||||||
return info
|
|
||||||
|
|
||||||
def mpv_extract_info(filename):
|
|
||||||
mp = MPV(ytdl=True)
|
|
||||||
mp.play(filename)
|
|
||||||
while mp.duration is None:
|
|
||||||
sleep(0.25)
|
|
||||||
if mp.playback_abort: return []
|
|
||||||
info = {'filename': filename, 'title': mp.media_title.decode(),
|
|
||||||
'duration': mp.osd.duration, 'error': False, 'playing': False,
|
|
||||||
'selected': False}
|
|
||||||
mp.quit()
|
|
||||||
return [info]
|
|
||||||
|
|
||||||
def ytdl_extract_info(filename):
|
|
||||||
with YoutubeDL(YTDL_OPTS) as ytdl:
|
|
||||||
raw_info = ytdl.extract_info(filename, download=False)
|
|
||||||
info = raw_info.get('entries', [raw_info])
|
|
||||||
for i in info:
|
|
||||||
if 'webpage_url' in i:
|
|
||||||
i['filename'] = i['webpage_url']
|
|
||||||
elif (i['ie_key'] == 'Youtube'
|
|
||||||
or i['extractor'] == 'youtube'):
|
|
||||||
i['filename'] = 'https://youtu.be/' + i['id']
|
|
||||||
else:
|
|
||||||
i['filename'] = i['url']
|
|
||||||
if 'title' not in i:
|
|
||||||
i['title'] = ytdl.extract_info(i['filename'],
|
|
||||||
download=False)['title']
|
|
||||||
if 'duration' not in i:
|
|
||||||
i['duration'] = '00:00:00'
|
|
||||||
elif isinstance(i['duration'], int):
|
|
||||||
i['duration'] = strftime('%H:%M:%S', gmtime(i['duration']))
|
|
||||||
for k in 'error', 'playing', 'selected': i.setdefault(k, False)
|
|
||||||
for k in i.copy():
|
|
||||||
if k not in DEFAULT_ENTRY: i.pop(k)
|
|
||||||
return info
|
|
||||||
|
|
||||||
try:
|
|
||||||
if (extractor != 'youtube-dl' and head(filename).status_code >= 400
|
|
||||||
and isfile(expanduser(expandvars(filename)))):
|
|
||||||
filename = abspath(expanduser(expandvars(filename)))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if extractor == 'json':
|
|
||||||
return json_extract_info(filename)
|
|
||||||
elif extractor == 'mpv':
|
|
||||||
return mpv_extract_info(filename)
|
|
||||||
else:
|
|
||||||
return ytdl_extract_info(filename)
|
|
||||||
|
|
||||||
|
|
||||||
class Omp(object):
|
class Omp(object):
|
||||||
"""Meta object for playing and playlist management.
|
"""Omni Media Player meta object.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
entries (list): list of all tracks
|
entries (list): list of all tracks
|
||||||
|
@ -174,7 +102,7 @@ class Omp(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def next(self, force=False, backward=False):
|
def next(self, force=False, backward=False):
|
||||||
comp.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:
|
21
setup.py
21
setup.py
|
@ -1,8 +1,7 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from os import walk
|
from os import listdir
|
||||||
from os.path import join
|
from os.path import join
|
||||||
from sys import prefix
|
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
|
@ -11,8 +10,8 @@ with open('README.rst') as f:
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='comp',
|
name='comp',
|
||||||
version='0.3.1',
|
version='0.3.2',
|
||||||
description=('Curses Online 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',
|
||||||
author='Nguyễn Gia Phong',
|
author='Nguyễn Gia Phong',
|
||||||
|
@ -29,16 +28,10 @@ setup(
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
'Topic :: Multimedia :: Sound/Audio :: Players',
|
'Topic :: Multimedia :: Sound/Audio :: Players',
|
||||||
'Topic :: Multimedia :: Video :: Display'
|
'Topic :: Multimedia :: Video :: Display'],
|
||||||
],
|
|
||||||
keywords='youtube-dl mpv-wrapper curses console-application multimedia',
|
keywords='youtube-dl mpv-wrapper curses console-application multimedia',
|
||||||
|
packages=['omp'],
|
||||||
install_requires=['python-mpv', 'youtube-dl'],
|
install_requires=['python-mpv', 'youtube-dl'],
|
||||||
data_files=[
|
package_data={'omp': ['locale/*/LC_MESSAGES/omp.mo']},
|
||||||
*((join(prefix, 'share', i[0]), [join(i[0], 'comp.mo')])
|
|
||||||
for i in walk('locale') if i[2]),
|
|
||||||
('/etc/comp', ['settings.ini'])
|
|
||||||
],
|
|
||||||
py_modules=['omp'],
|
|
||||||
scripts=['comp'],
|
scripts=['comp'],
|
||||||
platforms=['POSIX']
|
platforms=['POSIX'])
|
||||||
)
|
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,322 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"duration": "00:05:21",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/weeI1G46q0o",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "DJ Khaled - I'm the One ft. Justin Bieber, Quavo, Chance the Rapper, Lil Wayne"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:04:23",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/JGwWNGJdvx8",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "Ed Sheeran - Shape of You [Official Video]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:03:30",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/PMivT7MJ41M",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "Bruno Mars - That’s What I Like [Official Video]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:04:46",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/CTFtOOh47oo",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "French Montana - Unforgettable ft. Swae Lee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:04:45",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/NLZRYQMLDW4",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Kendrick Lamar - DNA."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:04:07",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/FM7MFYoylVs",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "The Chainsmokers & Coldplay - Something Just Like This (Lyric)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:03:48",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/72UO0v5ESUo",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "Luis Fonsi, Daddy Yankee - Despacito (Audio) ft. Justin Bieber"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/D5drYkLiLI8",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Kygo, Selena Gomez - It Ain't Me (with Selena Gomez) (Audio)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/Zgmvg-zzctI",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Lil Uzi Vert - XO TOUR Llif3 (Produced By TM88)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:12:53",
|
||||||
|
"error": false,
|
||||||
|
"filename": "test/gplv3.ogg",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "gplv3.ogg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/xvZqHgFz51I",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Future - Mask Off"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/8j9zMok6two",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Miley Cyrus - Malibu (Official Video)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/dPI-mRFEIH0",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Katy Perry - Bon Appétit (Official) ft. Migos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/aatr_2MstrI",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Clean Bandit - Symphony feat. Zara Larsson [Official Video]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:34:38",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://www.tube8.com/teen/nicole-ray-and-james-deen/409802/",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "Nicole Ray and James Deen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/7F37r50VUTQ",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "ZAYN, Taylor Swift - I Don’t Wanna Live Forever (Fifty Shades Darker)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/qFLhGq0060w",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "The Weeknd - I Feel It Coming ft. Daft Punk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/6ImFf__U6io",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "Birdman - Dark Shades (Explicit) ft. Lil Wayne, Mack Maine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:03:56",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://www.youtube.com/watch?v=3M3xfu0m5o4",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "David Banner - Play (Dirty version)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/NGLxoKOvzu4",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Jason Derulo - Swalla (feat. Nicki Minaj & Ty Dolla $ign) (Official Music Video)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/Hm1YFszJWbQ",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Migos - Slippery feat. Gucci Mane [Official Video]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/SC4xMk98Pdc",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Post Malone - Congratulations ft. Quavo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/nfs8NYg7yQM",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Charlie Puth - Attention [Official Video]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:04:10",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://www.youtube.com/watch?v=sRIkXM8S1J8",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "Best Goat Song Versions Compilation Ever! (HD)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/Dst9gZkq1a8",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Travis Scott - goosebumps ft. Kendrick Lamar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/dMK_npDG12Q",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Lorde - Green Light"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/h--P8HzYZ74",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "Zedd, Alessia Cara - Stay (Lyric Video)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/Mdh2p03cRfw",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Sam Hunt - Body Like A Back Road (Audio)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/Fq0xEpRDL9Q",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Chris Brown - Privacy (Explicit Version)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/7wtfhZwyrcc",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Imagine Dragons - Believer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/t_jHrUE5IOk",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Maluma - Felices los 4 (Official Video)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/wzZWXrlDj-A",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "DNCE - Kissing Strangers ft. Nicki Minaj"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/AEB6ibtdPZc",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Paramore: Hard Times [OFFICIAL VIDEO]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/vqW18C4plZ8",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "WizKid - Come Closer ft. Drake"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/A7xzXDStQnk",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Shawn Mendes - There's Nothing Holdin' Me Back (Lyric Video)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/FG9M0aEpJGE",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "G-Eazy & Kehlani - Good Life (from The Fate of the Furious: The Album) [MUSIC VIDEO]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/vp8VZe5kqEM",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Lady Gaga - The Cure (Audio)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/eP4eqhWc7sI",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Lana Del Rey - Lust For Life (Official Video) ft. The Weeknd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/5qJp6xlKEug",
|
||||||
|
"playing": false,
|
||||||
|
"selected": false,
|
||||||
|
"title": "Gorillaz - Saturnz Barz (Spirit House)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"duration": "00:00:00",
|
||||||
|
"error": false,
|
||||||
|
"filename": "https://youtu.be/9sg-A-eS6Ig",
|
||||||
|
"playing": false,
|
||||||
|
"selected": true,
|
||||||
|
"title": "Enrique Iglesias - SUBEME LA RADIO (Official Video) ft. Descemer Bueno, Zion & Lennox"
|
||||||
|
}
|
||||||
|
]
|
Loading…
Reference in New Issue