Clean up everything

This commit is contained in:
Nguyễn Gia Phong 2017-06-10 22:14:15 +07:00 committed by Nguyễn Gia Phong
parent 98c73ae8ac
commit 9129a8974f
13 changed files with 614 additions and 157 deletions

View File

@ -1,2 +1 @@
# Include the license file
include LICENSE README.rst
include LICENSE README.rst doc/screenshot.png

View File

@ -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.
.. image:: https://ipfs.io/ipfs/QmVhz4F53Sym48kXC7vhDMFsfvJ7iL8gaQ1EgoQADJvuAB
.. image:: doc/screenshot.png
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
``curses`` module is supported (i.e. Unix-like OS, e.g. GNU/Linux, macOS and
the BSDs).
Using pip
^^^^^^^^^
It also depends on ``youtube-dl`` and ``libmpv``. Both of those should be
available in your operating system's repository, although it's more
recommended to install ``youtube-dl`` using ``pip`` (currently most distros
still use Python 2 as default so the command is something like ``pip3 install
youtube-dl``).
Python 2 is still the default on most distributions so the command would be
``pip3 install comp``. You can use the ``--user`` flag to avoid system-wide
installation.
Installing comp
^^^^^^^^^^^^^^^
Using setup.py
^^^^^^^^^^^^^^
I will try to upload the program to PyPI when it's more completed but as of
this moment, I'd suggest you to use ``git`` to get the software::
To install the latest version or test the development branch (called
``bachelor``, in contrast to ``master``), you'll need to do it manually::
git clone https://github.com/McSinyx/comp.git
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
-----
@ -88,6 +91,8 @@ Keyboard control
+--------------+---------------------------------------------+
| ``d`` | Delete current entry |
+--------------+---------------------------------------------+
| ``i`` | Insert playlist |
+--------------+---------------------------------------------+
| ``m``, ``M`` | Cycle through playing modes |
+--------------+---------------------------------------------+
| ``n`` | Repeat previous search |
@ -120,8 +125,8 @@ Keyboard control
Configuration files
-------------------
The system-wide configuration file is ``/etc/comp/settings.ini``, the
user-specific one is ``~/.config/mpv/settings.ini``. Default configurations
If not specified by the ``--config``, (user-specific) configuration file is
``~/.config/mpv/settings.ini``. Default configurations
are listed below::
[comp]

87
comp
View File

@ -1,6 +1,5 @@
#!/usr/bin/env python3
#
# comp - Curses Online Media Player
# comp - Curses Omni Media Player
#
# comp is free software: you can redistribute it and/or modify
# 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 configparser import ConfigParser
from functools import reduce
from gettext import gettext as _, textdomain
from gettext import bindtextdomain, gettext as _, textdomain
from os import makedirs
from os.path import abspath, dirname, expanduser, isfile
from threading import Thread
from youtube_dl import YoutubeDL
from mpv import MPV
from pkg_resources import resource_filename
from youtube_dl import YoutubeDL
from omp import extract_info, Omp
# Init gettext
textdomain('comp')
bindtextdomain('omp', resource_filename('omp', 'locale'))
textdomain('omp')
# Global constants
SYSTEM_CONFIG = '/etc/comp/settings.ini'
@ -92,6 +93,8 @@ class Comp(Omp):
' ' if self.vid == 'no' else 'V')
adds(right.rjust(curses.COLS), curses.color_pair(12))
try:
self.played[self.playing]['duration'] = self.mp.osd.duration
self.print(self.played[self.playing])
left = ' {} / {} {} '.format(
self.mp.osd.time_pos, self.mp.osd.duration,
'|' if self.mp.pause else '>')
@ -151,7 +154,7 @@ class Comp(Omp):
def _writeln(self, y, title, duration, attr):
title_len = curses.COLS-DURATION_COL_LEN-3
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.refresh()
@ -188,7 +191,7 @@ class Comp(Omp):
def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
Omp.__init__(self, entries, lambda name, val: self.update_status(),
json_file, mode, mpv_vo, mpv_vid, ytdlf)
json_file, mode, mpv_vid, mpv_vo, ytdlf)
curses.noecho()
curses.cbreak()
self.scr.keypad(True)
@ -319,7 +322,7 @@ parser.add_argument('-e', '--extractor', default='youtube-dl',
choices=('json', 'mpv', 'youtube-dl'), required=False,
help=_("playlist extractor, default is youtube-dl"))
parser.add_argument('file', help=_("path or URL to the playlist to be opened"))
parser.add_argument('-c', '--config', required=False,
parser.add_argument('-c', '--config', default=USER_CONFIG, required=False,
help=_("path to the configuration file"))
parser.add_argument('--vid', required=False,
help=_("initial video channel. auto selects the default,\
@ -333,32 +336,12 @@ parser.add_argument('-f', '--format', required=False, metavar='YTDL_FORMAT',
args = parser.parse_args()
entries = extract_info(args.file, args.extractor)
json_file = args.file if args.extractor == 'json' else ''
config = ConfigParser()
if args.config is not None and isfile(args.config):
config_file = args.config
elif isfile(USER_CONFIG):
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)
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')
if args.format is not None:
ytdlf = args.format
else:
ytdlf = config.get('youtube-dl', 'format', fallback='best')
ytdlf = args.format or config.get('youtube-dl', 'format', fallback='best')
with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
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
try:
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:
errmsg = _("'{}': Can't open file for writing").format(
comp.json_file)
@ -407,14 +392,25 @@ with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
else:
comp.update_status(_("'{}' written").format(comp.json_file))
elif c == 100: # letter d
i = comp.idx()
if i + 1 < len(entries):
comp.entries.pop(i)
elif len(entries) > 1:
comp.entries.pop(i)
else:
comp.entries = []
comp.entries.pop(comp.idx())
if 1 < len(comp.entries) - curses.LINES + 4 == comp.start:
comp.start -= 1
elif comp.idx() == len(comp.entries):
comp.y -= 1
comp.redraw()
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
comp.mode = MODES[(MODES.index(comp.mode) + 1) % 8]
comp.update_status()
@ -422,9 +418,14 @@ with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
comp.next_search()
elif c == 111: # letter o
extractor = comp.gets(_("Playlist extractor: "))
comp.entries = extract_info(comp.gets(_("Open: ")), extractor)
comp.start, comp.y = 0, 1
comp.redraw()
filename = comp.gets(_("Open: "))
entries = extract_info(filename, extractor)
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
comp.mp.pause ^= True
elif c in (curses.KEY_UP, 107): # up arrow or letter k

BIN
doc/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 KiB

24
omp/__init__.py Normal file
View File

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

114
omp/ie.py Normal file
View File

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

View File

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

View File

@ -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
#
# 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
# 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/>.
@ -22,8 +21,8 @@ from collections import deque
from itertools import cycle
from os.path import abspath, expanduser, expandvars, isfile
from random import choice
from requests import head
from time import gmtime, sleep, strftime
from urllib import request
from youtube_dl import YoutubeDL
from mpv import MPV, MpvFormat
@ -34,79 +33,8 @@ YTDL_OPTS = {'quiet': True, 'default_search': 'ytsearch',
'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):
"""Meta object for playing and playlist management.
"""Omni Media Player meta object.
Attributes:
entries (list): list of all tracks
@ -174,7 +102,7 @@ class Omp(object):
pass
def next(self, force=False, backward=False):
comp.play_backward = backward
self.play_backward = backward
if self.mp.idle_active:
self.play(force)
else:

View File

@ -1,8 +1,7 @@
#!/usr/bin/env python3
from os import walk
from os import listdir
from os.path import join
from sys import prefix
from setuptools import setup
@ -11,8 +10,8 @@ with open('README.rst') as f:
setup(
name='comp',
version='0.3.1',
description=('Curses Online Media Player'),
version='0.3.2',
description=('Curses Omni Media Player'),
long_description=long_description,
url='https://github.com/McSinyx/comp',
author='Nguyễn Gia Phong',
@ -29,16 +28,10 @@ setup(
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Multimedia :: Sound/Audio :: Players',
'Topic :: Multimedia :: Video :: Display'
],
'Topic :: Multimedia :: Video :: Display'],
keywords='youtube-dl mpv-wrapper curses console-application multimedia',
packages=['omp'],
install_requires=['python-mpv', 'youtube-dl'],
data_files=[
*((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'],
package_data={'omp': ['locale/*/LC_MESSAGES/omp.mo']},
scripts=['comp'],
platforms=['POSIX']
)
platforms=['POSIX'])

File diff suppressed because one or more lines are too long

BIN
test/gplv3.ogg Normal file

Binary file not shown.

322
test/playlist.json Normal file
View File

@ -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 - 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",
"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 Dont 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"
}
]