#!/usr/bin/env python

"""
mps-youtube.

https://github.com/np1/mps-youtube

Copyright (C) 2014 nagev

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""

from __future__ import print_function

__version__ = "0.01.30"
__author__ = "nagev"
__license__ = "GPLv3"

import subprocess
import threading
import tempfile
import logging
import random
import socket
import time
import pafy
import json
import sys
import re
import os

try:
    # pylint: disable=F0401
    from colorama import init as init_colorama, Fore, Style
    has_colorama = True

except ImportError:
    has_colorama = False


# Python 3 compatibility hack

if sys.version_info[:2] >= (3, 0):
    # pylint: disable=E0611,F0401
    import pickle
    from urllib.request import build_opener
    from urllib.error import HTTPError, URLError
    from urllib.parse import urlencode
    py2utf8_encode = lambda x: x
    py2utf8_decode = lambda x: x
    compat_input = input

else:
    from urllib2 import build_opener, HTTPError, URLError
    import cPickle as pickle
    from urllib import urlencode
    py2utf8_encode = lambda x: x.encode("utf8")
    py2utf8_decode = lambda x: x.decode("utf8")
    compat_input = raw_input

mswin = os.name == "nt"
member_var = lambda x: not(x.startswith("__") or callable(x))


def mswinenc(txt):
    """ Encoding for Windows. """

    if mswin:
        sse = sys.stdout.encoding
        txt = txt.encode(sse, "replace").decode("utf8", "ignore")

    return txt


def mswinfn(filename):
    """ Fix filename for Windows. """

    if mswin:
        filename = mswinenc(filename)
        allowed = re.compile(r'[^\\/?*$\'"%&:<>|]')
        filename = "".join(x if allowed.match(x) else "_" for x in filename)

    return filename


def get_default_ddir():
    """ Get system default Download directory, append PMS dir. """

    if mswin:
        return os.path.join(os.path.expanduser("~"), "Downloads", "PMS")

    USER_DIRS = os.path.join(os.path.expanduser("~"),
                             ".config", "user-dirs.dirs")
    DOWNLOAD_HOME = os.path.join(os.path.expanduser("~"), "Downloads")

    if 'XDG_DOWNLOAD_DIR' in os.environ:
        ddir = os.environ['XDG_DOWNLOAD_DIR']

    elif os.path.exists(USER_DIRS):
        lines = open(USER_DIRS).readlines()
        defn = [x for x in lines if x.startswith("XDG_DOWNLOAD_DIR")]

        if len(defn) == 0:
            ddir = os.path.expanduser("~")

        else:
            ddir = defn[0].split("=")[1].replace('"', '')\
                .replace("$HOME", os.path.expanduser("~")).strip()

    elif os.path.exists(DOWNLOAD_HOME):
        ddir = DOWNLOAD_HOME

    else:
        ddir = os.path.expanduser("~")

    ddir = py2utf8_decode(ddir)
    return os.path.join(ddir, "PMS")


def get_config_dir():
    """ Get user's configuration directory. """

    if mswin:
        confdir = os.environ["APPDATA"]

    else:
        if 'XDG_CONFIG_HOME' in os.environ:
            confdir = os.environ['XDG_CONFIG_HOME']

        else:
            confdir = os.path.join(os.path.expanduser("~"), '.config')

    if not os.path.exists(confdir):
        os.mkdir(confdir)

    confdir = os.path.join(confdir, "pms-youtube")

    if not os.path.exists(confdir):
        os.mkdir(confdir)

    return confdir


class Config(object):

    """ Holds various configuration values. """

    PLAYER = "mplayer"
    PLAYERARGS = "-nolirc -nocache -prefer-ipv4 -really-quiet".split()
    COLOURS = False if mswin and not has_colorama else True
    CHECKUPDATE = True
    SHOW_MPLAYER_KEYS = True
    FULLSCREEN = False
    SHOW_STATUS = True
    DDIR = get_default_ddir()
    SHOW_VIDEO = False
    SEARCH_MUSIC = True


class Playlist(object):

    """ Representation of a playist, has list of songs. """

    def __init__(self, name=None, songs=None):
        self.name = name
        self.creation = time.time()
        self.songs = songs or []

    @property
    def is_empty(self):
        """ Return True / False if songs are populated or not. """

        return bool(not self.songs)

    @property
    def size(self):
        """ Return number of tracks. """

        return len(self.songs)

    @property
    def duration(self):
        """ Sum duration of the playlist. """

        duration = 0

        for song in self.songs:
            duration += int(song['duration'])

        duration = time.strftime('%H:%M:%S', time.gmtime(int(duration)))
        return duration


class g(object):

    """ Class for holding globals that are needed throught the module. """

    urlopen = None
    blank_text = "\n" * 200
    helptext = []
    max_results = 19
    max_retries = 8
    url_memo = {}
    model = Playlist(name="model")
    last_search_query = ""
    current_page = 1
    active = Playlist(name="active")
    noblank = False
    text = {}
    userpl = {}
    last_opened = message = content = ""
    config = [x for x in sorted(dir(Config)) if member_var(x)]
    configbool = [x for x in config if type(getattr(Config, x)) is bool]
    defaults = {setting: getattr(Config, setting) for setting in config}
    suffix = "3" if sys.version_info[:2] >= (3, 0) else ""
    CFFILE = os.path.join(get_config_dir(), "config")
    PLFILE = os.path.join(get_config_dir(), "playlist" + suffix)
    playerargs_defaults = {
        "mplayer": {"def": ("-nocache -prefer-ipv4 -nolirc "
                            "-really-quiet").split(),
                    "fs": "-fs",
                    "novid": "-novideo"
                    },
        "mpv": {"def": "--really-quiet".split(),
                "fs": "--fs",
                "novid": "--no-video"
                }
    }


def init():
    """ Initial setup. """

    init_debug()
    init_text()
    init_readline()
    init_opener()

    if not has_config() and has_mpv():
        Config.PLAYER = "mpv"
        set_playerargs(default=True)
        saveconfig()

    else:
        import_config()

    # setup colorama
    if has_colorama and mswin:
        init_colorama()


def init_debug():
    """ Enable debug logging if pmsytdebug env var exists. """
    if os.environ.get("pmsytdebug"):
        g.blank_text = "--\n"
        logfile = os.path.join(tempfile.gettempdir(), "pmsyt.log")
        logging.basicConfig(level=logging.DEBUG, filename=logfile)
        logging.getLogger("pafy").setLevel(logging.DEBUG)
    else:
        logging.basicConfig(level=logging.FATAL)


def init_readline():
    """ Enable readline for input history. """

    if not mswin:
        try:
            import readline  # import readline if not running on windows
            readline.get_history_length()  # redundant, prevents import warning

        except ImportError:
            pass  # no biggie


def init_opener():
    """ Set up url opener. """

    opener = build_opener()
    ua = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; "
    ua += "Trident/5.0)"
    opener.addheaders = [("User-Agent", ua)]
    g.urlopen = opener.open


def set_playerargs(playername=None, default=True):
    """ Set default arguments for player. """

    if not playername:
        playername = Config.PLAYER

    gpd = g.playerargs_defaults
    for player in gpd:

        # set player defaults
        if player in playername and default:
            Config.PLAYERARGS = gpd[player]["def"]

        # set fullscreen
        fsarg = gpd[player]["fs"]
        if player in playername:
            if Config.FULLSCREEN:

                if not fsarg in Config.PLAYERARGS:
                    Config.PLAYERARGS.append(fsarg)

            else:
                if fsarg in Config.PLAYERARGS:
                    Config.PLAYERARGS.pop(Config.PLAYERARGS.index(fsarg))

            break

    else:
        Config.PLAYERARGS = []


def has_config():
    """ Check whether config file has been saved before. """

    return os.path.exists(g.CFFILE)


def has_known_player():
    """ Return true if the set player is known. """

    for allowed_player in g.playerargs_defaults:
        regex = r'(?:^%s$)|(?:\b%s$)' % ((allowed_player,) * 2)
        match = re.search(regex, Config.PLAYER)

        if mswin:
            match = re.search(regex, Config.Player, re.IGNORECASE)

        if match:
            return True

    return False


def has_mpv():
    """ Check whether mpv is available. """

    if mswin:
        return False

    paths = "/usr/bin/ /usr/local/bin/".split()
    paths += os.environ.get("PATH", []).split(os.pathsep)

    for path in paths:
        mpvpath = os.path.join(path, "mpv")

        if os.path.exists(mpvpath):
            if os.path.isfile(mpvpath):
                if os.access(mpvpath, os.X_OK):
                    return True


def showconfig(_):
    """ Dump config data. """

    s = "  %s%-17s%s : \"%s\"\n"
    out = "  %s%-17s   %s%s%s\n" % (c.ul, "Key", "Value", " " * 40, c.w)

    for setting in g.config:
        val = getattr(Config, setting)

        # don't show playerargs as a list
        if setting == "PLAYERARGS":
            val = " ".join(val)

        # don't show fullscreen if unknown player
        if not has_known_player() and setting == "FULLSCREEN":
            continue

        out += s % (c.g, setting.lower(), c.w, val)

    g.content = out
    g.message = "Enter %sset <key> <value>%s to change\n" % (c.g, c.w)
    g.message += "Enter %sset all default%s to reset all" % (c.g, c.w)


def saveconfig():
    """ Save current config to file. """

    config = {setting: getattr(Config, setting) for setting in g.config}
    pickle.dump(config, open(g.CFFILE, "wb"), protocol=2)


def import_config():
    """ Override config if config file exists. """

    if os.path.exists(g.CFFILE):
        saved_config = pickle.load(open(g.CFFILE, "rb"))

        for k, v in saved_config.items():
            setattr(Config, k, v)

        # Update config files from versions <= 0.01.08
        # pylint: disable=E1103
        if type(Config.PLAYERARGS) == str:
            Config.PLAYERARGS = Config.PLAYERARGS.split(" ")
            saveconfig()


class c(object):

    """ Class for holding colour code values. """

    if mswin and has_colorama:
        white = Style.RESET_ALL
        ul = Style.DIM + Fore.YELLOW
        red, green, yellow = Fore.RED, Fore.GREEN, Fore.YELLOW
        blue, pink = Fore.CYAN, Fore.MAGENTA

    elif mswin:
        Config.COLOURS = False

    else:
        white = "\x1b[%sm" % 0
        ul = "\x1b[%sm" * 3 % (2, 4, 33)
        cols = ["\x1b[%sm" % n for n in range(91, 96)]
        red, green, yellow, blue, pink = cols

    if not Config.COLOURS:
        ul = red = green = yellow = blue = pink = white = ""
    r, g, y, b, p, w = red, green, yellow, blue, pink, white


def setconfig(key, val):
    """ Set configuration variable. """

    # pylint: disable=R0912
    success_msg = fail_msg = ""
    key = key.upper()
    val_orig = val
    val = val.upper()
    TRUE = (Config, key, True)
    FALSE = (Config, key, False)
    falses = "0 FALSE OFF NO".split()

    # set all default
    if key == "ALL" and val == "DEFAULT":

        for k, v in g.defaults.items():
            setattr(Config, k, v)
            success_msg = "Default configuration reinstated"

    elif key == "PLAYER" and val == "DEFAULT":
        player = "mpv" if has_mpv() else g.defaults['PLAYER']
        setattr(Config, key, player)
        set_playerargs()
        success_msg = "PLAYER has been set to %s" % val

    elif key == "COLOURS" and mswin and not has_colorama:
        fail_msg = "Can't enable colours, colorama not found"

    elif key == "PLAYER":
        setattr(Config, key, val_orig)
        set_playerargs(default=True)
        success_msg = "PLAYER has been set to %s" % val_orig

    elif key == "PLAYERARGS" and not val == "DEFAULT":
        setattr(Config, key, val_orig.split())
        success_msg = "PLAYERARGS set to %s" % val_orig

    elif key == "PLAYERARGS" and val == "DEFAULT":
        set_playerargs(default=True)
        success_msg = "PLAYERARGS set to %s" % val

    elif key == "FULLSCREEN" and not has_known_player():
        fail_msg = "Unknown player %s, set fullscreen manually."
        fail_msg = fail_msg % Config.PLAYER

    elif key == "FULLSCREEN" and val == "DEFAULT":
        setattr(*FALSE)
        set_playerargs()
        success_msg = "FULLSCREEN set to disabled (default)"

    elif key == "DDIR" and not val == "DEFAULT":
        valid = os.path.exists(val_orig) and os.path.isdir(val_orig)

        if valid:
            setattr(Config, key, val_orig)
            success_msg = "Downloads will be saved to %s%s%s"
            success_msg = success_msg % (c.y, val_orig, c.w)

        else:
            fail_msg = "Invalid path: %s%s%s" % (c.r, val, c.w)

    elif key in g.configbool and not val == "DEFAULT":

        boolval = val not in falses

        if key == "FULLSCREEN":
            if boolval:
                setattr(*TRUE)

            else:
                setattr(*FALSE)

            set_playerargs(default=False)
            success_msg = "FULLSCREEN set to %s" % boolval

        elif not boolval:
            setattr(*FALSE)
            success_msg = "%s set to disabled (restart may be required)" % key
        else:
            setattr(*TRUE)
            success_msg = "%s set to enabled (restart may be required)" % key

    elif key in g.configbool and val == "DEFAULT":
        boolval = val not in falses
        setattr(Config, key, boolval)
        success_msg = "%s set to %s (restart may be required)" % (key, boolval)

    elif key in g.config:

        if val == "DEFAULT":
            val = g.defaults[key]

            if key == "FULLSCREEN":
                set_playerargs(default=False)

        setattr(Config, key, val)
        success_msg = "%s has been set to %s" % (key.upper(), val)

    else:
        fail_msg = "Unknown config item: %s%s%s" % (c.r, key, c.w)

    showconfig(1)

    if success_msg:
        saveconfig()
        g.message = success_msg

    elif fail_msg:
        g.message = fail_msg


g.helptext = [("Basic Usage", """
{0}Basic Usage{1}

Use {2}/{1} or {2}.{1} to prefix your search query.  e.g., {2}/pink floyd{1}

{2}<number(s)>{1} - play specified items, separated by commas.
            e.g., {2}1-3,5{1} plays items 1, 2, 3 and 5.

{2}i <number>{1} - view information on the specified item.

{2}d <number>{1} - download the specified item.

[For further help, enter a number from the list below]
""".format(c.ul, c.w, c.g, c.r)),
    ("Searching and Editing Results", """
{0}Searching and Editing Results{1}

Use {2}/{1} or {2}.{1} to prefix your search query.  e.g., {2}/daft punk{1}
{2}set search_music false{1} - search all YouTube categories.
{2}set search_music true{1} - search only YouTube music category.
{2}n{1} and {2}p{1} - continue search to next/previous pages.
{2}/<username> -user{1} - list YouTube uploads by <username>.

{2}rm <number(s)>{1} - remove items from displayed results.
{2}sw <number>,<number>{1} - swap two items.
{2}mv <number>,<number>{1} - move item <number> to position <number>.
""".format(c.ul, c.w, c.g, c.r)),

    ("Downloading and Playback", """
{0}Downloading and Playback{1}

{2}set show_video true{1} - play video instead of audio.
{2}<number(s)>{1} - play specified items, separated by commas.
            e.g., {2}1-3,5{1} plays items 1, 2, 3 and 5
{2}i <number>{1} - view information on the specified item.
{2}d <number>{1} - view downloads available for an item.
{2}da <number>{1} - download best available audio file.
{2}dv <number>{1} - download best available video file.

{2}all{1} - play all displayed items.
{2}shuffle <number(s)>{1} - play specified items in random order.
{2}repeat <number(s)>{1} - play and repeat the specified items.

Note: {2}all{1} can be used as an argument (e.g., {2}shuffle all{1})
""".format(c.ul, c.w, c.g, c.r)),

    ("Using Playlists", """
{0}Using Playlists{1}

{2}add <number(s)>{1} - add items to the current playlist.
{2}add <number(s)> <playlist>{1} - add items to the specified playlist.
{2}vp{1} - view current playlist.
{2}ls{1} - list your saved playlists.
{2}open <name or ID>{1} - open a saved playlist as the current playlist.
{2}view <name or ID>{1} - view a playlist (current playlist left intact)
{2}play <name or ID>{1} - play a saved playlist directly.
{2}save{1} or {2}save <name>{1} - save the displayed items as a playlist.
{2}rmp <playlist_name or ID>{1} - delete a playlist from disk.
{2}mv <old name or ID> <new name>{1} - rename a playlist.
""".format(c.ul, c.w, c.g, c.r)),

    ("Invocation Parameters", """
{0}Invocation{1}

To play a saved playlist when invoking mpsyt use {2}mpsyt play <name>{1}
This also works for other commands:

{2}mpsyt /mozart{1} to search
{2}mpsyt open <name or ID>{1} to open a saved playlist
{2}mpsyt ls{1} to list saved playlists
""".format(c.ul, c.w, c.g, c.r)),

    ("Advanced Tips", """
{0}Advanced Tips{1}
When using {2}open{1}, {2}view{1} or {2}play{1} to access a playlist, you \
can enter the
first few characters instead of the whole name. The first alphabetically
matching playlist will be opened/displayed/played.

Use {2}5-{1} to select items 5 upward and {2}-5{1} to select up to item 5. \
This can be
included with other choices. e.g., 5,3,7-,-2
You can use spaces instead of commas: 5 3 7- -2

Use {2}-w{1}, {2}-f{1} or {2}-a{1} with your choice to override the configured\
 setting and
play items in windowed, fullscreen or audio modes respectively.

{2}set{1} - view current configuration.
{2}set fullscreen true{1} - play video content in fullscreen mode.
{2}set player mpv{1} or {2} - set player mplayer{1} change player application
""".format(c.ul, c.w, c.g, c.r))
]


def F(key, nb=0, na=0, percent=r"\*", nums=r"\*\*", textlib=None):
    """Format text.

    nb, na indicate newlines before and after to return
    percent is the delimter for %s
    nums is the delimiter for the str.format command (**1 will become {1})
    textlib is the dictionary to use (defaults to g.text if not given)

    """

    textlib = textlib or g.text

    assert key in textlib
    text = textlib[key]
    percent_fmt = textlib.get(key + "_")
    number_fmt = textlib.get("_" + key)

    if number_fmt:
        text = re.sub(r"(%s(\d))" % nums, "{\\2}", text)
        text = text.format(*number_fmt)

    if percent_fmt:
        text = re.sub(r"%s" % percent, r"%s", text)
        text = text % percent_fmt

    text = re.sub(r"&&", r"%s", text)

    return "\n" * nb + text + c.w + "\n" * na


def init_text():
    """ Set up text. """

    g.text = {

        "exitmsg": ("**0mps - **1http://github.com/np1/mps-youtube**0\n"
                    "Released under the GPLv3 license\n"
                    "(c) 2014 nagev**2\n"""),
        "_exitmsg": (c.r, c.b, c.w),

        # Error / Warning messages

        'no playlists': "*No saved playlists found!*",
        'no playlists_': (c.r, c.w),
        'pl bad name': '*&&* is not valid a valid name. Ensure it starts with'
        ' a letter or _',
        'pl bad name_': (c.r, c.w),
        'pl not found': 'Playlist *&&* unknown. Saved playlists are shown '
        'above',
        'pl not found_': (c.r, c.w),
        'pl not found advise ls': 'Playlist "*&&*" not found. Use *ls* to '
        'list',
        'pl not found advise ls_': (c.y, c.w, c.g, c.w),
        'pl empty': 'Playlist is empty!',
        'advise add': 'Use *add N* to add a track',
        'advise add_': (c.g, c.w),
        'advise search': 'Search for items and then use *add* to add them',
        'advise search_': (c.g, c.w),
        'no data': 'Error fetching data. Possible network issue.'
        '\n*&&*',
        'no data_': (c.r, c.w),
        'use dot': 'Start your query with a *.* to perform a search',
        'use dot_': (c.g, c.w),
        'cant get track': 'Problem fetching this track: *&&*',
        'cant get track_': (c.r, c.w),
        'track unresolved': 'Sorry, this track is not available',
        'no player': '*&&* was not found on this system',
        'no player_': (c.y, c.w),
        'no pl match for rename': '*Couldn\'t find matching playlist to '
        'rename*',
        'no pl match for rename_': (c.r, c.w),
        'invalid range': "*Invalid item / range entered!*",
        'invalid range_': (c.r, c.w),

        # Info messages

        'pl renamed': 'Playlist *&&* renamed to *&&*',
        'pl renamed_': (c.y, c.w, c.y, c.w),
        'pl saved': 'Playlist saved as *&&*.  Use *ls* to list playlists',
        'pl saved_': (c.y, c.w, c.g, c.w),
        'pl loaded': 'Loaded playlist *&&* as current playlist',
        'pl loaded_': (c.y, c.w),
        'pl viewed': 'Showing playlist *&&*',
        'pl viewed_': (c.y, c.w),
        'pl help': 'Enter *open <name or ID>* to load a playlist',
        'pl help_': (c.g, c.w),
        'added to pl': '*&&* tracks added (*&&* total [*&&*]). Use *vp* to '
        'view',
        'added to pl_': (c.y, c.w, c.y, c.w, c.y, c.w, c.g, c.w),
        'added to saved pl': '*&&* tracks added to *&&* (*&&* total [*&&*])',
        'added to saved pl_': (c.y, c.w, c.y, c.w, c.y, c.w, c.y, c.w),
        'song move': 'Moved *&&* to position *&&*',
        'song move_': (c.y, c.w, c.y, c.w),
        'song sw': ("Switched item *&&* with *&&*"),
        'song sw_': (c.y, c.w, c.y, c.w),
        'current pl': "This is the current playlist. Use *save <name>* to save"
        " it",
        'current pl_': (c.g, c.w),
        'songs rm': '*&&* tracks removed &&',
        'songs rm_': (c.y, c.w)
        }


def save_to_file():
    """ Save playlists.  Called each time a playlist is saved or deleted. """

    f = open(g.PLFILE, "wb")
    pickle.dump(g.userpl, f, protocol=2)


def open_from_file():
    """ Open playlists. Called once on script invocation. """

    try:
        f = open(g.PLFILE, "rb")
        g.userpl = pickle.load(f)

    except IOError:
        g.userpl = {}
        save_to_file()


def logo(col=None, version=""):
    """ Return text logo. """

    col = col if col else random.choice((c.g, c.r, c.y, c.b, c.p, c.w))
    LOGO = col + """\

                88888b.d88b.  88888b.  .d8888b
                888 "888 "88b 888 "88b 88K
                888  888  888 888  888 "Y8888b.
                888  888  888 888 d88P      X88
                888  888  888 88888P"   88888P'
                              888
                              888   %s%s
                              888%s%s
""" % (c.w + "v" + version + " (YouTube)" if version else "", col, c.w, "\n\n")

    return LOGO + c.w


def playlists_display():
    """ Produce a list of all playlists. """

    if not g.userpl:
        g.message = F("no playlists")
        return logo(c.y) + "\n\n" if g.model.is_empty else \
            generate_songlist_display()

    maxname = max(len(a) for a in g.userpl)
    out = "      {0}Saved Playlists{1}\n".format(c.ul, c.w)
    start = "      "
    fmt = "%s%s%-3s %-" + str(maxname + 3) + "s%s %s%-7s%s %-5s%s"
    head = (start, c.b, "ID", "Name", c.b, c.b, "Count", c.b, "Duration", c.w)
    out += "\n" + fmt % head + "\n\n"

    for v, z in enumerate(sorted(g.userpl)):
        n, p = z, g.userpl[z]
        l = fmt % (start, c.g, v + 1, n, c.w, c.y, str(p.size), c.y,
                   p.duration, c.w) + "\n"
        out += l

    return out


def mplayer_help(short=True):
    """ Mplayer help.  """

    volume = "[{0}9{1}] volume [{0}0{1}]"
    volume = volume if short else volume + "      [{0}ctrl-c{1}] return"
    seek = u"[{0}\u2190{1}] seek [{0}\u2192{1}]"
    pause = u"[{0}\u2193{1}] SEEK [{0}\u2191{1}]       [{0}space{1}] pause"

    if mswin:
        seek = "[{0}<-{1}] seek [{0}->{1}]"
        pause = "[{0}DN{1}] SEEK [{0}UP{1}]       [{0}space{1}] pause"

    ret = "[{0}q{1}] %s" % ("return" if short else "next track")
    fmt = "    %-20s       %-20s"
    lines = fmt % (seek, volume) + "\n" + fmt % (pause, ret)
    return lines.format(c.g, c.w)


def tidy(raw, field):
    """ Tidy HTML entities, format songlength if field is duration.  """

    if field == "duration":
        raw = time.strftime('%H:%M:%S', time.gmtime(int(raw)))
        H, M, S = raw.split(":")

        if H == "00":
            raw = M + ":" + S

        elif H == "01" and int(M) < 40:
            raw = str(int(M) + 60) + ":" + S

        elif H.startswith("0"):
            raw = ":".join([H[1], M, S])

    else:
        for r in (("&#039;", "'"), ("&amp;#039;", "'"), ("&amp;amp;", "&"),
                 ("  ", " "), ("&amp;", "&"), ("&quot;", '"')):
            raw = raw.replace(r[0], r[1])

    return raw


def get_tracks_from_json(jsons):
    """ Get search results from web page. """

    try:
        items = jsons['data']['items']

    except KeyError:
        items = []

    songs = []

    for item in items:
        cursong = dict(
            title=item['title'].strip(),
            duration=item['duration'],
            length=tidy(item['duration'], "duration"),
            link=item['id'],
            rating=item.get('rating')
        )

        songs.append(cursong)

    if not items:
        logging.debug("got unexpected data or no search results")
        return False

    return songs


def screen_update():
    """ Display content, show message, blank screen."""

    if not g.noblank:
        print(g.blank_text)

    if g.content:
        g.content = mswinenc(g.content)
        print(py2utf8_encode(g.content))

    if g.message:
        print(g.message)

    g.message = g.content = False
    g.noblank = False


def playback_progress(idx, allsongs, repeat=False):
    """ Generate string to show selected tracks, indicate current track. """

    # pylint: disable=R0914
    # too many local variables
    out = "  %s%-71s%s%s\n" % (c.ul, "Title", "Time", c.w)
    show_key_help = (Config.PLAYER == "mplayer" or Config.PLAYER == "mpv")\
        and Config.SHOW_MPLAYER_KEYS
    multi = len(allsongs) > 1

    for n, song in enumerate(allsongs):
        length = " " * (8 - len(song['length'])) + song['length']
        i = song['title'][:64], length, song['length']
        fmt = (c.w, "  ", c.b, i[0], c.w, c.y, i[1], c.w)

        if n == idx:
            fmt = (c.y, "> ", c.p, i[0], c.w, c.p, i[1], c.w)
            cur = i

        out += "%s%s%s%-66s%s %s%s%s\n" % fmt

    out += "\n" * (3 - len(allsongs))
    pos = 8 * " ", c.y, idx + 1, c.w, c.y, len(allsongs), c.w
    playing = "{}{}{}{} of {}{}{}\n\n".format(*pos) if multi else "\n\n"
    keys = mplayer_help(short=(not multi and not repeat))
    out = out if multi else generate_songlist_display(song=allsongs[0])

    if show_key_help:
        out += "\n" + keys

    else:
        playing = "{}{}{}{} of {}{}{}\n".format(*pos) if multi else "\n"
        out += "\n" + " " * 58 if multi else ""

    fmt = playing, c.r, cur[0], c.w, c.w, cur[2], c.w
    out += "%s    %s%s%s %s[%s]%s" % fmt
    out += "    REPEAT MODE" if repeat else ""
    return out


def generate_songlist_display(song=False, zeromsg=None):
    """ Generate list of choices from a song list."""

    songs = g.model.songs or []

    if not songs:
        g.message = zeromsg or "Enter /search-term to search or [h]elp"
        return logo(c.g) + "\n\n"

    fmtrow = "%s%-5s %-63s %-6s  %s\n"
    fmthd = "%s%-5s %-65s %-6s%s\n"
    head = (c.ul, "Item", "Title", "Length", c.w)
    out = "\n" + fmthd % head

    for n, x in enumerate(songs):
        col = (c.r if n % 2 == 0 else c.p) if not song else c.b
        length = x.get('length') or 0
        length = " " * (8 - len(length)) + length
        title = x.get('title') or "unknown title"

        if not song or song != songs[n]:
            out += (fmtrow % (col, str(n + 1), title[:63], str(length), c.w))

        else:
            out += (fmtrow % (c.p, str(n + 1), title[:63], str(length), c.w))

    return out + "\n" * (5 - len(songs)) if not song else out


def writestatus(text):
    """ Update status linei. """

    if Config.SHOW_STATUS:
        writeline(text)


def writeline(text):
    """ Print text on same line. """

    spaces = 75 - len(text)
    sys.stdout.write(" " + text + (" " * spaces) + "\r")
    sys.stdout.flush()


def get_stream_size(song, video=False):
    """ Get size of stream. """

    if not video:
        stream = song['pafy'].getbestaudio()
        key = "audiosize"

    else:
        stream = song['pafy'].getbest()
        key = "videosize"

    if song.get(key):
        return song[key]

    if not stream:
        return 0

    else:
        size = stream.get_filesize()
        return size


def get_streams(song, force=False, future=False):
    """ Return the pafy object for a song. """

    if not force and "pafy" in song and song['pafy'].expires > time.time():
        logging.info("cache hit for %s", song['title'])
        return song['pafy']

    else:
        statusline = "getting stream url for %s..." % song['link']

        if not future:
            writestatus(statusline)
            p = pafy.new(song['link'], callback=writestatus)
            logging.info("resolving url stream for %s", song['title'])

        else:
            p = pafy.new(song['link'])
            logging.info("--prefetched url for %s", song['title'])

        return p


def playsong(song, failcount=0, override=False):
    """ Play song using config.PLAYER called with args config.PLAYERARGS."""
    # pylint: disable=R0914
    # too many local variables
    # pylint: disable=R0912
    # too many branches

    try:
        song['pafy'] = get_streams(song, force=failcount)
        paf = song['pafy']

    except (IOError, URLError, HTTPError, socket.timeout) as e:
        g.message = F('cant get track') % str(e)
        return

    except ValueError:
        g.message = F('track unresolved')
        return

    video = Config.SHOW_VIDEO

    # handle audio / video override

    if override == "fullscreen" or override == "window":
        video = True

    elif override == "audio":
        video = False

    stream = paf.getbest() if video else paf.getbestaudio()

    # handle no audio stream found
    if not stream and not video:
        override = "a-v"
        video = True  # to display correct status info
        stream = paf.getbest()

    stream.url = stream.url.replace("https://", "http://")
    size = get_stream_size(song, video=video)
    key = "audiosize" if not video else "videosize"
    song[key] = size
    songdata = song['link'], stream.extension, int(size / (1024 ** 2))
    args = Config.PLAYERARGS[::]

    if has_known_player():

        # handle no audio stream available
        if override == "a-v":
            args.append(g.playerargs_defaults[Config.PLAYER]["novid"])

        # handle fullscreen / window overrides
        elif override == "fullscreen" and not Config.FULLSCREEN:
            args.append(g.playerargs_defaults[Config.PLAYER]["fs"])

        elif override == "window" and Config.FULLSCREEN:
            fsarg = g.playerargs_defaults[Config.PLAYER]["fs"]
            args.pop(args.index(fsarg))

    cmd = [Config.PLAYER] + args + [stream.url]
    stdout = stderr = None
    now = time.time()
    writestatus("%s; %s; %s Mb" % songdata)

    with open(os.devnull, "w") as fnull:

        if "mpv" in Config.PLAYER or "mplayer" in Config.PLAYER:
            stderr = fnull

        if mswin:
            stdout = stderr = fnull

        try:
            logging.info("playing %s (%s)", paf.title, failcount)
            subprocess.call(cmd, stdout=stdout, stderr=stderr)

        except OSError:
            g.message = F('no player') % Config.PLAYER
            return

    fin = time.time()
    failed = fin - now < 1 and int(paf.length) > 10

    if failed and failcount < g.max_retries:
        del song['pafy']
        logging.warn("stream failed to open")
        writestatus("trying again (attempt %s)" % (2 + failcount))
        failcount += 1
        playsong(song, failcount=failcount)

    if not failed:
        save_to_file()


def _uploads(userterm, query):
    """ Modify query dict for user uploaded videos. """

    term = userterm.strip()
    term = term.replace("-user", "")
    term = term.strip()

    if " " in term:
        return {}, ""

    else:
        del query['q']

        if query.get('category'):
            del query['category']

        query['orderby'] = 'published'

    return query, term


def _search(url, progtext, qs=None, splash=True):
    """ Perform memoized url fetch, display progtext. """

    g.message = "Searching for '%s%s%s'" % (c.y, progtext, c.w)

    # attach query string if supplied
    url = url + "?" + urlencode(qs) if qs else url

    # use cached value if exists
    if url in g.url_memo:
        songs = g.url_memo[url]

    # show splash screen during fetch
    else:
        if splash:
            g.content = logo(c.b) + "\n\n"
            screen_update()

        # perform fetch
        try:
            wdata = g.urlopen(url).read().decode("utf8")
            wdata = json.loads(wdata)
            songs = get_tracks_from_json(wdata)

        except (URLError, HTTPError) as e:
            g.message = F('no data') % e
            g.content = logo(c.r)
            return

    if songs:
        # preload first result url
        kwa = {"song": songs[0], "delay": 0}
        t = threading.Thread(target=preload, kwargs=kwa)
        t.start()

        # cache resuls
        g.url_memo[url] = songs
        g.model.songs = songs
        return True

    return False


def search(term, page=1, splash=True):
    """ Perform search. """

    if not term or len(term) < 2:
        g.message = c.r + "Not enough input" + c.w
        g.content = generate_songlist_display()

    else:
        original_term = term
        logging.info("search for %s", original_term)
        url = "https://gdata.youtube.com/feeds/api/videos"

        query = {
            'q': term,
            'v': 2,
            'alt': 'jsonc',
            'start-index': ((page - 1) * g.max_results + 1) or 1,
            'safeSearch': "none",
            'max-results': g.max_results,
            'orderby': 'relevance'
        }
        if Config.SEARCH_MUSIC:
            query['category'] = "Music"

        if "-user" in term:
            query, user = _uploads(term, query)

            if not user:
                return False

            url = "https://gdata.youtube.com/feeds/api/users/%s/uploads"
            url = url % user

        have_results = _search(url, original_term, query)

        if have_results:
            g.message = "Search results for %s%s%s" % (c.y, original_term, c.w)

            if "/users/" in url:
                g.message = "Video uploads by %s%s%s" % (c.y, user, c.w)

            g.last_opened = ""
            g.last_search_query = original_term
            g.current_page = page
            g.content = generate_songlist_display()

        else:
            g.message = "Found nothing for %s%s%s" % (c.y, term, c.w)
            g.content = logo(c.r)
            g.current_page = 1
            g.last_search_query = ""


def _make_fname(song, ext=None, av=None):
    """" Create download directory, generate filename. """

    if not os.path.exists(Config.DDIR):
        os.makedirs(Config.DDIR)

    p = get_streams(song)

    if ext:
        extension = ext

    elif av == "audio":
        extension = p.getbestaudio().extension

    else:
        extension = p.getbest().extension

    filename = song['title'][:59] + "." + extension
    filename = os.path.join(Config.DDIR, mswinfn(filename.replace("/", "-")))
    return filename


def _download(song, filename, url=None, audio=False):
    """ Download file, show status, return filename. """
    # pylint: disable=R0914
    # too many local variables

    print("Downloading to %s%s%s ..\n" % (c.r, filename, c.w))
    status_string = ('  {0}{1:,}{2} Bytes [{0}{3:.2%}{2}] received. Rate: '
                     '[{0}{4:4.0f} kbps{2}].  ETA: [{0}{5:.0f} secs{2}]')

    if not url:
        paf = get_streams(song)
        video = not audio
        url = paf.getbest().url if video else paf.getbestaudio().url

    resp = g.urlopen(url)
    total = int(resp.info()['Content-Length'].strip())
    chunksize, bytesdone, t0 = 16384, 0, time.time()
    outfh = open(filename, 'wb')

    while True:
        chunk = resp.read(chunksize)
        outfh.write(chunk)
        elapsed = time.time() - t0
        bytesdone += len(chunk)
        rate = (bytesdone / 1024) / elapsed
        eta = (total - bytesdone) / (rate * 1024)
        stats = (c.y, bytesdone, c.w, bytesdone * 1.0 / total, rate, eta)

        if not chunk:
            outfh.close()
            break

        status = status_string.format(*stats)
        sys.stdout.write("\r" + status + ' ' * 4 + "\r")
        sys.stdout.flush()

    return filename


def _bi_range(start, end):
    """
    Inclusive range function, works for reverse ranges.

    eg. 5,2 returns [5,4,3,2] and 2, 4 returns [2,3,4]

    """
    if start == end:
        return (start,)

    elif end < start:
        return reversed(range(end, start + 1))

    else:
        return range(start, end + 1)


def _parse_multi(choice, end=None):
    """ Handle ranges like 5-9, 9-5, 5- and -5. Return list of ints. """

    end = end or str(g.model.size)
    pattern = r'(?<![-\d])(\d+-\d+|-\d+|\d+-|\d+)(?![-\d])'
    items = re.findall(pattern, choice)
    alltracks = []

    for x in items:

        if x.startswith("-"):
            x = "1" + x

        elif x.endswith("-"):
            x = x + str(end)

        if "-" in x:
            nrange = x.split("-")
            startend = map(int, nrange)
            alltracks += _bi_range(*startend)

        else:
            alltracks.append(int(x))

    return alltracks


def _get_near_plname(begin):
    """ Return the closest matching playlist name that starts with begin. """

    for name in sorted(g.userpl):
        if name.lower().startswith(begin.lower()):
            break
    else:
        return begin

    return name


def play_pl(name):
    """ Play a playlist by name. """

    if name.isdigit():
        name = int(name)
        name = sorted(g.userpl)[name - 1]

    saved = g.userpl.get(name)

    if not saved:
        name = _get_near_plname(name)
        saved = g.userpl.get(name)

    if saved:
        g.model.songs = list(saved.songs)
        play_all("", "", "")

    else:
        g.message = F("pl not found") % name
        g.content = playlists_display()
        #return


def save_last(args=None):
    """ Save command with no playlist name. """

    if g.last_opened:
        open_save_view("save", g.last_opened)

    else:
        saveas = ""

        #save using artist name in postion 1
        if not g.model.is_empty:
            saveas = g.model.songs[0]['title'][:18].strip()
            saveas = re.sub(r"[^-\w]", "-", saveas, re.UNICODE)

        # loop to find next available name
        post = 0

        while g.userpl.get(saveas):
            post += 1
            saveas = g.model.songs[0]['title'][:18].strip() + "-" + str(post)

        open_save_view("save", saveas)


def open_save_view(action, name):
    """ Open, save or view a playlist by name.  Get closest name match. """

    if action == "open" or action == "view":

        saved = g.userpl.get(name)

        if not saved:
            name = _get_near_plname(name)
            saved = g.userpl.get(name)

        if saved and action == "open":
            g.model.songs = g.active.songs = list(saved.songs)
            g.message = F("pl loaded") % name
            g.last_opened = name
            g.content = generate_songlist_display()

        elif saved and action == "view":
            g.model.songs = list(saved.songs)
            g.message = F("pl viewed") % name
            g.last_opened = ""
            g.content = generate_songlist_display()

        elif not saved and action in "view open".split():
            g.message = F("pl not found") % name
            g.content = playlists_display()

    elif action == "save":

        if not g.model.songs:
            g.message = "Nothing to save. " + F('advise search')
            g.content = generate_songlist_display()

        else:
            name = name.replace(" ", "-")
            g.userpl[name] = Playlist(name, list(g.model.songs))
            g.message = F('pl saved') % name
            save_to_file()
            g.content = generate_songlist_display()


def open_view_bynum(action, num):
    """ Open or view a saved playlist by number. """

    srt = sorted(g.userpl)
    name = srt[int(num) - 1]
    open_save_view(action, name)


def songlist_rm_add(action, songrange):
    """ Remove or add tracks. works directly on user input. """

    selection = _parse_multi(songrange)

    if action == "add":

        for songnum in selection:
            g.active.songs.append(g.model.songs[songnum - 1])

        d = g.active.duration
        g.message = F('added to pl') % (len(selection), g.active.size, d)

    elif action == "rm":
        selection = list(reversed(sorted(list(set(selection)))))
        removed = str(tuple(reversed(selection))).replace(",", "")

        for x in selection:
            g.model.songs.pop(x - 1)

        g.message = F('songs rm') % (len(selection), removed)

    g.content = generate_songlist_display()


def play(pre, choice, post=""):
    """ Play choice.  Use repeat/random if appears in pre/post. """
    # pylint: disable=R0914
    # too many local variables

    if not g.model.songs:
        g.message = c.r + "There are no tracks to select" + c.w
        g.content = g.content or generate_songlist_display()

    else:
        shuffle = "shuffle" in pre + post
        repeat = "repeat" in pre + post
        novid = "-a" in pre + post
        fs = "-f" in pre + post
        nofs = "-w" in pre + post

        if (novid and fs) or (novid and nofs) or (nofs and fs):
            raise IOError("Conflicting override options specified")

        override = False
        override = "audio" if novid else override
        override = "fullscreen" if fs else override
        override = "window" if nofs else override

        selection = _parse_multi(choice)
        songlist = [g.model.songs[x - 1] for x in selection]

        # cache next result of displayed items
        if len(songlist) == 1:
            chosen = selection[0] - 1

            if len(g.model.songs) > chosen + 1:
                nx = g.model.songs[chosen + 1]
                kwa = {"song": nx, "override": override}
                t = threading.Thread(target=preload, kwargs=kwa)
                #t = threading.Thread(target=preload, args=(nx, override))
                t.start()

        logging.debug("calling play range with override set to %s", override)
        play_range(songlist, shuffle, repeat, override)


def play_all(pre, choice, post=""):
    """ Play all tracks in model (last displayed). shuffle/repeat if req'd."""

    options = pre + choice + post
    play(options, "1-" + str(len(g.model.songs)))


def ls():
    """ List user saved playlists. """

    if not g.userpl:
        g.message = F('no playlists')
        g.content = g.content or generate_songlist_display(zeromsg=g.message)

    else:
        g.content = playlists_display()
        g.message = F('pl help')


def vp():
    """ View current working playlist. """

    if g.active.is_empty:
        txt = F('advise search') if g.model.is_empty else F('advise add')
        g.message = F('pl empty') + " " + txt

    else:
        g.model.songs = g.active.songs
        g.message = F('current pl')

    g.content = generate_songlist_display()


def preload(song, delay=1, override=False):
    """  Get streams (runs in separate thread). """

    time.sleep(delay)
    song['pafy'] = get_streams(song, future=True)
    video = Config.SHOW_VIDEO

    if override == "fullscreen" or override == "window":
        video = True

    elif override == "audio":
        video = False

    size = get_stream_size(song, video=video)

    if video:
        song['videosize'] = size

    else:
        song['audiosize'] = size


def play_range(songlist, shuffle=False, repeat=False, override=False):
    """ Play a range of songs, exit cleanly on keyboard interrupt. """

    if shuffle:
        random.shuffle(songlist)

    if not repeat:

        for n, song in enumerate(songlist):
            g.content = playback_progress(n, songlist, repeat=False)
            screen_update()

            hasnext = len(songlist) > n + 1

            if hasnext:
                nex = songlist[n + 1]
                kwa = {"song": nex, "override": override}
                t = threading.Thread(target=preload, kwargs=kwa)
                t.start()

            try:
                playsong(song, override=override)

            except KeyboardInterrupt:
                logging.info("Keyboard Interrupt")
                print("Stopping...")
                time.sleep(1)
                g.message = c.y + "Playback halted" + c.w
                break

    elif repeat:

        while True:
            try:
                for n, song in enumerate(songlist):
                    g.content = playback_progress(n, songlist, repeat=True)
                    screen_update()
                    playsong(song, override=override)
                    g.content = generate_songlist_display()

            except KeyboardInterrupt:
                print("Stopping...")
                time.sleep(2)
                g.message = c.y + "Playback halted" + c.w
                break

    g.content = generate_songlist_display()


def show_help(option="1"):
    """ Print help message. """

    all_help = g.helptext
    help_titles = ["\n[%s] %s" % (n + 1, x[0]) for n, x in enumerate(all_help)]
    help_titles = "".join(help_titles)

    if option.isdigit() and int(option) <= len(all_help):
        g.content = all_help[int(option) - 1][1] + help_titles
        g.message = "[{0}1-%s{1} for more or {0}ENTER{1} to return]"
        g.message = " " * 23 + g.message.format(c.y, c.w) % len(all_help)
        screen_update()

        try:
            inp = compat_input("help > " + c.y)
            show_help(inp)

        except (KeyboardInterrupt, EOFError):
            prompt_for_exit()

        g.content = generate_songlist_display()


def quits(showlogo=True):
    """ Exit the program. """

    msg = g.blank_text + logo(c.r, version=__version__) if showlogo else ""
    vermsg = ""
    print(msg + F("exitmsg", 2))

    if Config.CHECKUPDATE and showlogo:
        try:
            url = "https://github.com/np1/mps-youtube/raw/master/VERSION"
            v = g.urlopen(url).read().decode("utf8")
            v = re.search(r"^version\s*([\d\.]+)\s*$", v, re.MULTILINE)
            if v:
                v = v.group(1)
                if v > __version__:
                    vermsg += "\nA newer version is available (%s)\n" % v
        except (URLError, HTTPError, socket.timeout):
            pass

    sys.exit(vermsg)


def get_dl_data(song):
    """ Get filesize and metadata for all streams, return dict. """

    mbsize = lambda x: str(int(x / (1024 ** 2)))
    p = get_streams(song)
    song['pafy'] = p
    dldata = []
    text = " [Fetching stream info] >"
    l = len(p.allstreams)
    for n, stream in enumerate(p.allstreams):
        sys.stdout.write(text + "-" * n + ">" + " " * (l - n - 1) + "<\r")
        sys.stdout.flush()
        size = mbsize(stream.get_filesize())

        item = {'mediatype': stream.mediatype,
                'size': size,
                'ext': stream.extension,
                'quality': stream.quality,
                'notes': getattr(stream, "notes", ""),  # getattr for backward
                                                        # pafy compatibility
                'url': stream.url}

        dldata.append(item)

    writestatus("")
    return dldata


def menu_prompt(model, prompt=" > ", rows=None, header=None, theading=None,
                footer=None, force=0):
    """ Generate a list of choice, returns item from model. """

    content = ""

    for x in header, theading, rows, footer:
        if type(x) == list:

            for line in x:
                content += line + "\n"

        elif type(x) == str:
            content += x + "\n"

    g.content = content
    screen_update()

    choice = compat_input(prompt)

    if choice in model:
        return model[choice]

    else:
        if force:
            return menu_prompt(model, prompt, rows, header, theading, footer,
                               force)

    return False, False


def prompt_dl(song):
    """ Prompt user do choose a stream to dl.  Return (url, extension). """

    dl_data = get_dl_data(song)
    dl_text = gen_dl_text(dl_data, song)

    model = [x['url'] for x in dl_data]
    ed = enumerate(dl_data)
    model = {str(n + 1): (x['url'], x['ext']) for n, x in ed}
    url, ext = menu_prompt(model, "Download number: ", *dl_text)
    return url, ext


def gen_dl_text(ddata, song):
    """ Generate text for dl screen. """

    hdr = []
    hdr.append("  %s%s%s [%s]" % (c.r, song['title'], c.w,
                                  song['pafy'].duration))
    author = py2utf8_decode(song['pafy'].author)
    hdr.append(c.r + "  Uploaded by " + author + c.w)
    hdr.append("")

    heading = tuple("Item Format Quality Media Size Notes".split())
    fmt = "  {0}%-6s %-8s %-13s %-7s   %-5s   %-16s{1}"
    heading = [fmt.format(c.w, c.w) % heading]
    heading.append("")

    content = []
    for n, d in enumerate(ddata):
        row = (n + 1, d['ext'], d['quality'], d['mediatype'], d['size'],
               d['notes'])
        fmt = "  {0}%-6s %-8s %-13s %-7s %5s Mb   %-16s{1}"
        row = fmt.format(c.g, c.w) % row
        content.append(row)

    content.append("")

    footer = "Select [%s1-%s%s] to download or [%sEnter%s] to return"
    footer = [footer % (c.y, len(content) - 1, c.w, c.y, c.w)]
    return(content, hdr, heading, footer)


def download(dltype, num):
    """ Download a track. """

    song = (g.model.songs[int(num) - 1])
    best = dltype.startswith("dv") or dltype.startswith("da")

    if not best:
        url, ext = prompt_dl(song)

        if not url:
            g.content = generate_songlist_display()
            g.message = "%sNo download selected%s" % (c.y, c.w)
            return

        else:
            filename = _make_fname(song, ext)
            args = (song, filename, url)
            kwargs = {}

    elif best:
        av = "audio" if dltype.startswith("da") else "video"
        audio = av == "audio"
        filename = _make_fname(song, None, av=av)
        args = (song, filename)
        kwargs = dict(url=None, audio=audio)

    try:
        f = _download(*args, **kwargs)
        g.message = "Downloaded " + c.g + f + c.w

    except IndexError:
        g.message = c.r + "Invalid index" + c.w

    except KeyboardInterrupt:
        g.message = c.r + "Download halted!" + c.w

        try:
            os.remove(filename)

        except IOError:
            pass

    finally:
        g.content = generate_songlist_display()


def prompt_for_exit():
    """ Ask for exit confirmation. """

    g.message = c.r + "Press ctrl-c again to exit" + c.w
    g.content = generate_songlist_display()
    screen_update()

    try:
        userinput = compat_input(c.r + " > " + c.w)

    except (KeyboardInterrupt, EOFError):
        quits(showlogo=False)

    return userinput


def playlist_remove(name):
    """ Delete a saved playlist by name - or purge working playlist if *all."""

    if name.isdigit() or g.userpl.get(name):

        if name.isdigit():
            name = int(name) - 1
            name = sorted(g.userpl)[name]

        del g.userpl[name]
        g.message = "Deleted playlist %s%s%s" % (c.y, name, c.w)
        g.content = playlists_display()
        save_to_file()

    else:
        g.message = F('pl not found advise ls') % name
        g.content = playlists_display()


def songlist_mv_sw(action, a, b):
    """ Move a song or swap two songs. """

    i, j = int(a) - 1, int(b) - 1

    if action == "mv":
        g.model.songs.insert(j, g.model.songs.pop(i))
        g.message = F('song move') % (g.model.songs[j]['title'], b)

    elif action == "sw":
        g.model.songs[i], g.model.songs[j] = g.model.songs[j], g.model.songs[i]
        g.message = F('song sw') % (min(a, b), max(a, b))

    g.content = generate_songlist_display()


def playlist_add(nums, playlist):
    """ Add selected song nums to saved playlist. """

    nums = _parse_multi(nums)

    if not g.userpl.get(playlist):
        playlist = playlist.replace(" ", "-")
        g.userpl[playlist] = Playlist(playlist)

    for songnum in nums:
        g.userpl[playlist].songs.append(g.model.songs[songnum - 1])
        dur = g.userpl[playlist].duration
        f = (len(nums), playlist, g.userpl[playlist].size, dur)
        g.message = F('added to saved pl') % f
        save_to_file()

    g.content = generate_songlist_display()


def playlist_rename_idx(_id, name):
    """ Rename a playlist by ID. """

    _id = int(_id) - 1
    playlist_rename(sorted(g.userpl)[_id] + " " + name)


def playlist_rename(playlists):
    """ Rename a playlist using mv command. """

    # Deal with old playlist names that permitted spaces
    a, b = "", playlists.split(" ")
    while a not in g.userpl:
        a = (a + " " + (b.pop(0))).strip()
        if not b and not a in g.userpl:
            g.message = F('no pl match for rename')
            g.content = g.content or playlists_display()
            return

    b = "-".join(b)
    g.userpl[b] = Playlist(b)
    g.userpl[b].songs = list(g.userpl[a].songs)
    playlist_remove(a)
    g.message = F('pl renamed') % (a, b)
    save_to_file()


def add_rm_all(action):
    """ Add all displayed songs to current playlist.

    remove all displayed songs from view.

    """

    if action == "rm":
        for n in reversed(range(0, len(g.model.songs))):
            g.model.songs.pop(n)
        g.message = c.b + "Cleared all songs" + c.w
        g.content = generate_songlist_display()

    elif action == "add":
        size = g.model.size
        songlist_rm_add("add", "-" + str(size))


def nextprev(np):
    """ Get next / previous search results. """

    if np == "n":
        if len(g.model.songs) == g.max_results and g.last_search_query:
            g.current_page += 1
            search(g.last_search_query, g.current_page, splash=False)
            g.message += " : page %s" % g.current_page

        else:
            g.message = "No more items to display"

    elif np == "p":
        if g.current_page > 1 and g.last_search_query:
            g.current_page -= 1
            search(g.last_search_query, g.current_page, splash=False)
            g.message += " : page %s" % g.current_page

        else:
            g.message = "No previous items to display"

    g.content = generate_songlist_display()


def info(num):
    """ Get video description. """

    writestatus("Fetching video metadata..")
    item = (g.model.songs[int(num) - 1])
    p = get_streams(item)
    item['pafy'] = p
    i = py2utf8_decode
    writestatus("Fetched")
    out = c.ul + "Video Info" + c.w + "\n\n"
    out += py2utf8_decode(p.title or "")
    out += "\n" + (p.description or "")
    out += py2utf8_decode("\n\nAuthor     : " + str(p.author))
    out += py2utf8_decode("\nView count : " + str(p.viewcount))
    out += i("\nRating     : " + str(p.rating)[:4])
    out += i("\nCategory   : " + p.category)
    out += i("\nLink       : " + "https://youtube.com/watch?v=%s" % p.videoid)
    out += i("\n\n%s[%sPress enter to go back%s]%s" % (c.y, c.w, c.y, c.w))
    g.content = out


def plist(parturl):
    """ Import playlist created on website. """

    # not implemented
    g.content = generate_songlist_display()


def main():
    """ Main control loop. """

    g.content = generate_songlist_display()
    g.content = logo(col=c.g, version=__version__) + "\n"
    g.message = "Enter /search-term to search or [h]elp"
    screen_update()

    # open playlists from file
    open_from_file()

    # get cmd line input
    inp = " ".join(sys.argv[1:])

    # input types
    word = r'[^\W\d][-\w\s]{,100}'
    rs = r'(?:repeat\s*|shuffle\s*|-a\s*|-f\s*|-w\s*)'
    regx = {
        'ls': r'ls$',
        'vp': r'vp$',
        'top': r'top(|3m|6m|year|all)\s*$',
        #'plist': r'.*(list[\da-zA-Z]{8,14})$',
        'play': r'(%s{0,3})([-,\d\s]{1,250})\s*(%s{0,3})$' % (rs, rs),
        'info': r'i\s*(\d{1,4})$',
        'quits': r'(?:q|quit|exit)$',
        'search': r'(?:search|\.|/)\s*(.{2,500})',
        'play_pl': r'play\s*(%s|\d+)$' % word,
        'download': r'(dv|da|d|dl|download)\s*(\d{1,4})$',
        'nextprev': r'(n|p)$',
        'play_all': r'(%s{0,3})(?:\*|all)\s*(%s{0,3})$' % (rs, rs),
        'save_last': r'(save)\s*$',
        'setconfig': r'set\s*(\w+)\s*"?([^"]*)"?\s*$',
        'show_help': r'(?:help|h)$',
        'add_rm_all': r'(rm|add)\s(?:\*|all)$',
        'showconfig': r'(set|showconfig)\s*$',
        'playlist_add': r'add\s*(-?\d[-,\d\s]{1,250})(%s)$' % word,
        'open_save_view': r'(open|save|view)\s*(%s)$' % word,
        'songlist_mv_sw': r'(mv|sw)\s*(\d{1,4})\s*[\s,]\s*(\d{1,4})$',
        'songlist_rm_add': r'(rm|add)\s*(-?\d[-,\d\s]{,250})$',
        'playlist_rename': r'mv\s*(%s\s+%s)$' % (word, word),
        'playlist_remove': r'rmp\s*(\d+|%s)$' % word,
        'open_view_bynum': r'(open|view)\s*(\d{1,4})$',
        'playlist_rename_idx': r'mv\s*(\d{1,3})\s*(%s)\s*$' % word
    }

    # compile regexp's
    regx = {name: re.compile(val, re.UNICODE) for name, val in regx.items()}
    prompt = "> " + c.y if not mswin else "> "

    while True:
        try:
            # get user input
            userinput = inp or compat_input(prompt)
            userinput = userinput.strip()
            print(c.w)

        except (KeyboardInterrupt, EOFError):
            userinput = prompt_for_exit()

        inp = None

        for k, v in regx.items():
            if v.match(userinput):
                func, matches = k, v.match(userinput).groups()

                try:
                    globals()[func](*matches)

                except IndexError:
                    g.message = F('invalid range')
                    g.content = g.content or generate_songlist_display()

                except IOError as e:
                    g.message = F('cant get track') % str(e)
                    g.content = g.content or generate_songlist_display()

                break
        else:
            g.content = g.content or generate_songlist_display()

            if userinput:
                g.message = c.b + "Bad syntax. Enter h for help" + c.w

        screen_update()


if __name__ == "__main__":
    init()
    main()
