#!/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.38"
__author__ = "nagev"
__license__ = "GPLv3"

import unicodedata
import collections
import subprocess
import threading
import tempfile
import logging
import random
import locale
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"
non_utf8 = mswin or not "UTF-8" in os.environ.get("LANG", "")
dbg = logging.debug
member_var = lambda x: not(x.startswith("__") or callable(x))
locale.setlocale(locale.LC_ALL, "")  # for date formatting


def non_utf8_encode(txt):
    """ Encoding for non UTF8 environments. """

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

    return txt


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

    if mswin:
        filename = non_utf8_encode(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 mps dir. """

    user_home = os.path.expanduser("~")
    join, exists = os.path.join, os.path.exists

    if mswin:
        return join(user_home, "Downloads", "mps")

    USER_DIRS = join(user_home, ".config", "user-dirs.dirs")
    DOWNLOAD_HOME = join(user_home, "Downloads")

    # define ddir by (1) env var, (2) user-dirs.dirs file,
    #                (3) existing ~/Downloads dir (4) ~

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

    elif 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 = user_home

        else:
            ddir = defn[0].split("=")[1].replace('"', '')
            ddir = ddir.replace("$HOME", user_home).strip()

    elif exists(DOWNLOAD_HOME):
        ddir = DOWNLOAD_HOME

    else:
        ddir = user_home

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


def get_config_dir():
    """ Get user's configuration directory. Migrate to new mps name if old."""

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

    elif 'XDG_CONFIG_HOME' in os.environ:
        confdir = os.environ['XDG_CONFIG_HOME']

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

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

    if os.path.exists(old_confdir) and not os.path.exists(mps_confdir):
        os.rename(old_confdir, mps_confdir)

    elif not os.path.exists(mps_confdir):
        os.makedirs(mps_confdir)

    return mps_confdir


class Video(object):

    """ Class to represent a YouTube video. """

    def __init__(self, ytid=None, title=None, length=None):
        self.ytid = ytid
        self.title = title
        self.length = int(length)
        self.urls = dict(v={}, a={})


def is_valid(video):
    """ Check with Video object has unexpired links. """

    last = video.urls['v'].get("last")  # 'v' and 'a' are fetched together
    valid = last and time.time() < last + g.expiry
    return valid


def reset_video(video):
    """ Clear url data of a Video object. """

    video.urls["v"]["last"] = 0
    video.urls["v"]["url"] = ""
    video.urls["a"]["last"] = 0
    video.urls["a"]["url"] = ""


def populate_video(video, callback=None):
    """ Populate Video object with pafy data.  Fetches new Pafy object. """

    nullf = lambda x: None
    callback = callback if callback else nullf
    callback("Fetching video...")

    try:
        dbg(" - pafy fetch for %s", video.title)
        g.pafs[video.ytid] = pafy.new(video.ytid, callback=callback)
        p = g.pafs[video.ytid]

    except (ValueError, IndexError):
        raise IOError("Can't fetch this stream")

    while len(g.pafs) > g.max_pafy_objects:
        g.pafs.popitem(last=False)  # store maximum 500 objects

    v = p.getbest()
    preftype = "ogg" if Config.PLAYER == "mplayer" else "m4a"
    strict = Config.PLAYER == "mplayer"
    a = p.getbestaudio(preftype=preftype, ftypestrict=strict)
    video.urls['v'] = dict(url=v.url, last=time.time(), ext=v.extension)

    if a:  # audio streams may not be available
        video.urls['a'] = dict(url=a.url, last=time.time(), ext=a.extension)


def get_streams(video, force=False, future=False):
    """ Check a video is valid, if not fetch new data. Return video. """

    if force or not is_valid(video):
        nullf = lambda x: None
        callback = nullf if future else writestatus
        populate_video(video, callback=callback)

    return video


def get_content_length(url):
    """ Return content length of a url. """

    dbg("getting content-length header")
    response = g.urlopen(url)
    headers = response.headers
    cl = headers['content-length']
    return int(cl)


def get_pafy(video):
    """ Get valid pafy object for song. """

    p = g.pafs.get(video.ytid)  # try to get pafy object from memory

    if not p or not is_valid(video):
        # fetch new pafy object if links expired or no pafy available
        populate_video(video)

    return g.pafs.get(video.ytid)


def get_size(video, is_video):
    """ Get size of item.  Store it in Video object. """

    key = "v" if is_video else "a"
    size = video.urls[key].get('size')

    if size:
        pass

    elif not is_valid(video):
        populate_video(video)

        if is_video:
            size = g.pafs[video.ytid].getbest().get_filesize()

        else:
            preftype = "ogg" if Config.PLAYER == "mplayer" else "m4a"
            strict = Config.PLAYER == "mplayer"
            a = g.pafs[video.ytid].getbestaudio(preftype=preftype,
                                                ftypestrict=strict)
            size = a.get_filesize() if a else 0

    else:
        url = video.urls[key].get("url")

        try:
            size = get_content_length(url) if url else 0

        except HTTPError:
            size = 0

    video.urls[key]['size'] = size
    return size


class Config(object):

    """ Holds various configuration values. """

    PLAYER = "mplayer"
    PLAYERARGS = "-nolirc -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 = sum(s.length for s in self.songs)
        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. """

    debug_mode = False
    urlopen = None
    ytpls = []
    browse_mode = "normal"
    preloading = []
    expiry = 5 * 60 * 60  # 5 hours
    blank_text = "\n" * 200
    helptext = []
    max_results = 19
    max_retries = 3
    max_pafy_objects = 500
    url_memo = {}
    model = Playlist(name="model")
    last_search_query = {}
    current_page = 1
    active = Playlist(name="active")
    noblank = False
    text = {}
    userpl = {}
    ytpl = {}
    pafs = collections.OrderedDict()
    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")
    OLD_PLFILE = os.path.join(get_config_dir(), "playlist" + suffix)
    PLFILE = os.path.join(get_config_dir(), "playlist_v2")
    playerargs_defaults = {
        "mpv": {"def": "--really-quiet".split(),
                "fs": "--fs",
                "novid": "--no-video",
                "ignidx": "--demuxer-lavf-o=fflags=+ignidx".split()
                },
        "mplayer": {"def": ("-prefer-ipv4 -nolirc -really-quiet").split(),
                    "fs": "-fs",
                    "novid": "-novideo",
                    #"ignidx": "-lavfdopts o=fflags=+ignidx".split()
                    "ignidx": [],
                    }
    }


def get_version_info():
    """ Return version and platform info. """

    import platform
    out = ("\nmpsyt version  : %s " % __version__)
    out += ("\npafy version   : %s" % pafy.__version__)
    out += ("\nPython version : %s" % sys.version)
    out += ("\nProcessor      : %s" % platform.processor())
    out += ("\nMachine type   : %s" % platform.machine())
    out += ("\nArchitecture   : %s, %s" % platform.architecture())
    out += ("\nPlatform       : %s" % platform.platform())
    return out


def process_cl_args(args):
    """ Process command line arguments. """

    if "--version" in args:
        print(get_version_info())
        print("")
        sys.exit()

    if "--debug" in args:
        init_debug(force=True)
        print("\nDEBUG MODE")
        print(get_version_info())

        for x in sys.argv:

            if "--debug" in x:
                sys.argv.remove(x)

    if "--help" in args:

        for x in g.helptext:
            print(x[1])

        sys.exit()


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

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

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

    else:
        import_config()

    # setup colorama
    if has_colorama and mswin:
        init_colorama()

    process_cl_args(sys.argv)


def init_debug(force=False):
    """ Enable debug logging if mpsytdebug var is set. """

    if os.environ.get("mpsytdebug") == "1" or force:
        g.debug_mode = True
        g.blank_text = "--\n"
        logfile = os.path.join(tempfile.gettempdir(), "mpsyt.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_exefile(filename):
    """ Check whether file exists in path and is executable. """

    paths = os.environ.get("PATH", []).split(os.pathsep)
    dbg("searching path for %s", filename)

    for path in paths:
        exepath = os.path.join(path, filename)

        if os.path.exists(exepath):
            if os.path.isfile(exepath):

                if os.access(exepath, os.X_OK):
                    dbg("found at %s", exepath)
                    return exepath

    return False


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_exefile("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


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-youtube - **1http://github.com/np1/mps-youtube**0"
                    "\nReleased 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 item: *&&*',
        '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),
        '-audio': "*Warning* - the filetype you selected (m4v) has no audio!",
        '-audio_': (c.y, c.w),

        # Info messages

        'select mux': ("Select [*&&*] to mux audio or [*Enter*] to download "
                       "without audio\nThis feature is experimental!"),
        'select mux_': (c.y, c.w, c.y, c.w),
        '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)
    dbg("Playlist saved\n---")


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

    try:
        with open(g.PLFILE, "rb") as plf:
            g.userpl = pickle.load(plf)

    except IOError:
        # no playlist found, create a blank one
        if not os.path.isfile(g.PLFILE):
            g.userpl = {}
            save_to_file()


def convert_playlist_to_v2():
    """ Convert previous playlist file to v2 playlist. """

    # skip if previously done
    if os.path.isfile(g.PLFILE):
        return

    # skip if no playlist files exist
    elif not os.path.isfile(g.OLD_PLFILE):
        return

    try:
        with open(g.OLD_PLFILE, "rb") as plf:
            old_playlists = pickle.load(plf)

    except IOError:
        sys.exit("Couldn't open old playlist file")

    #rename old playlist file
    backup = g.OLD_PLFILE + "_v1_backup"

    if os.path.isfile(backup):
        sys.exit("Error, backup exists but new playlist exists not!")

    os.rename(g.OLD_PLFILE, backup)

    # do the conversion
    for plname, plitem in old_playlists.items():

        songs = []

        for video in plitem.songs:
            v = Video(video['link'], video['title'], video['duration'])
            songs.append(v)

        g.userpl[plname] = Playlist(plname, songs)

    # save as v2
    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 non_utf8:
        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 fmt_time(seconds):
    """ Format number of seconds to %H:%M:%S. """

    hms = time.strftime('%H:%M:%S', time.gmtime(int(seconds)))
    H, M, S = hms.split(":")

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

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

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

    return hms


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 = Video(ytid=item['id'], title=item['title'].strip(),
                        length=int(item['duration']))

        songs.append(cursong)

    if not items:
        dbg("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 = non_utf8_encode(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_orig = fmt_time(song.length)
        length = " " * (8 - len(length_orig)) + length_orig
        i = uea_rpad(66, song.title), length, length_orig
        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%s%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" + " " * 61 if multi else ""

    fmt = playing, c.r, cur[0].strip()[:61], 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 real_len(u):
    """ Try to determine width of strings displayed with monospace font. """

    ueaw = unicodedata.east_asian_width
    widths = dict(W=2, F=2, A=1, N=0.75, H=0.5)
    return int(round(sum(widths.get(ueaw(char), 1) for char in u)))


def uea_trunc(num, t):
    """ Truncate to num chars taking into account East Asian width chars. """

    while real_len(t) > num:
        t = t[:-1]

    return t


def uea_rpad(num, t):
    """ Right pad with spaces taking into account East Asian width chars. """

    t = uea_trunc(num, t)

    if real_len(t) < num:
        t = t + (" " * (num - real_len(t)))

    return t


def generate_playlist_display():
    """ Generate list of playlists. """

    if not g.ytpls:
        g.message = c.r + "No playlists found!"
        return logo(c.g) + "\n\n"

    fmtrow = "%s%-5s %s %-8s  %-2s%s\n"
    fmthd = "%s%-5s %-57s %-9s %-5s%s\n"
    head = (c.ul, "Item", "Playlist", "Updated", "Count", c.w)
    out = "\n" + fmthd % head

    for n, x in enumerate(g.ytpls):
        col = (c.g if n % 2 == 0 else c.w)
        length = x.get('size') or "?"
        length = "%4s" % length
        title = x.get('title') or "unknown"
        updated = time.strptime(x.get('updated'), "%Y-%m-%dT%H:%M:%S.000Z")
        updated = time.strftime("%x", updated)
        title = uea_rpad(57, title)
        out += (fmtrow % (col, str(n + 1), title, updated, str(length), c.w))

    return out + "\n" * (5 - len(g.ytpls))


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

    if g.browse_mode == "ytpl":
        return generate_playlist_display()

    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 %s %-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 = fmt_time(x.length)
        length = " " * (8 - len(length)) + length
        title = x.title or "unknown title"
        title = uea_rpad(63, title)

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

        else:
            out += (fmtrow % (c.p, str(n + 1), title, 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 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

    # don't interrupt preloading:
    while song.ytid in g.preloading:
        writestatus("fetching item..")
        time.sleep(0.1)

    video = Config.SHOW_VIDEO
    video = True if override in ("fullscreen", "window") else video
    video = False if override == "audio" else video

    try:
        get_streams(song, force=failcount)

    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

    stream = song.urls['v']['url'] if video else song.urls['a'].get('url')
    aext = song.urls['a'].get("ext")

    # handle no audio stream available, or m4a with mplayer
    # by switching to video stream and suppressing video output.
    if any([not stream and not video,
            not video and Config.PLAYER == "mplayer" and aext == "m4a"]):

        dbg("no audio available or mplayer m4a, overriding to video")
        override = "a-v"
        video = True
        stream = song.urls['v']['url']

    url = stream.replace("https://", "http://")
    key = "a" if not video else "v"
    size = get_size(song, is_video=video)
    songdata = song.ytid, song.urls[key]['ext'], int(size / (1024 ** 2))
    args = Config.PLAYERARGS[::]

    if has_known_player():
        novid_arg = g.playerargs_defaults[Config.PLAYER]["novid"]
        fs_arg = g.playerargs_defaults[Config.PLAYER]["fs"]

        # handle no audio stream available
        if override == "a-v":
            args.append(novid_arg)

        # handle fullscreen / window overrides
        elif override == "fullscreen" and not Config.FULLSCREEN:
            args.append(fs_arg)

        elif override == "window" and Config.FULLSCREEN:
            args.pop(args.index(fs_arg))

        # prevent ffmpeg issue (https://github.com/mpv-player/mpv/issues/579)
        if not video and song.urls["a"].get("ext") == "m4a":
            args += g.playerargs_defaults[Config.PLAYER]["ignidx"]

    cmd = [Config.PLAYER] + args + [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)", song.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 song.length > 1

    if failed and failcount < g.max_retries:
        reset_video(song)
        logging.warn("stream failed to open")
        dbg("trying again (attempt %s)" % (2 + failcount))
        writestatus(F("cant get track") % song.title)
        time.sleep(1)
        failcount += 1
        playsong(song, failcount=failcount, override=override)

    if not failed:
        save_to_file()


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 generate_search_qs(term, page):
    """ Return query string. """

    qs = {
        'q': term,
        'v': 2,
        'alt': 'jsonc',
        'start-index': ((page - 1) * g.max_results + 1) or 1,
        'safeSearch': "none",
        'max-results': g.max_results,
        'paid-content': "false",
        'orderby': 'relevance'
    }

    if Config.SEARCH_MUSIC:
        qs['category'] = "Music"

    return qs


def usersearch(user, page=1, splash=True):
    """ Fetch uploads by a YouTube user. """

    query = generate_search_qs(user, page)
    del query['q']
    query['orderby'] = 'published'

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

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

    have_results = _search(url, user, query)

    if have_results:
        g.message = "Video uploads by %s%s%s" % (c.y, user, c.w)
        g.last_opened = ""
        g.last_search_query = {"user": user}
        g.current_page = page
        g.content = generate_songlist_display()

    else:
        g.message = "User %s%s%s not found" % (c.y, user, c.w)
        g.content = logo(c.r)
        g.current_page = 1
        g.last_search_query = {}


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()
        return

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

    if have_results:
        g.message = "Search results for %s%s%s" % (c.y, original_term, c.w)
        g.last_opened = ""
        g.last_search_query = {"term": original_term}
        g.browse_mode = "normal"
        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 pl_search(term, page=1, splash=True):
    """ Search for YouTube playlists. """

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

    logging.info("playlist search for %s", term)
    url = "https://gdata.youtube.com/feeds/api/playlists/snippets?"
    start = (page - 1) * g.max_results or 1
    qs = {"q": term, "start-index": start,
          "max-results": g.max_results, "v": 2, 'alt': 'jsonc'}
    url += urlencode(qs)

    if url in g.url_memo:
        playlists = g.url_memo[url]

    else:
        g.content = logo(c.g)
        g.message = "Searching playlists for %s" % c.y + term + c.w
        screen_update()
        page = g.urlopen(url).read().decode("utf8")
        pldata = json.loads(page)
        playlists = get_pl_from_json(pldata)

    if playlists:
        g.url_memo[url] = playlists[::]
        g.last_search_query = {"playlists": term}
        g.browse_mode = "ytpl"
        g.ytpls = playlists
        g.message = "Playlist results for %s" % c.y + term + c.w
        g.content = generate_playlist_display()

    else:
        g.message = "No playlists found for: %s" % c.y + term + c.w
        g.content = generate_songlist_display(zeromsg=g.message)


def get_pl_from_json(pldata):
    """ Process json playlist data. """

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

    except KeyError:
        items = []

    results = []
    for item in items:
        results.append(dict(link=item.get("id"),
                            size=item.get("size"),
                            title=item.get("title"),
                            author=item.get("author"),
                            created=item.get("created"),
                            updated=item.get("updated"),
                            description=item.get("description")
                            )
                       )
    return results


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

    # pylint: disable=E1103
    # Instance of 'bool' has no 'extension' member (some types not inferable)
    if not os.path.exists(Config.DDIR):
        os.makedirs(Config.DDIR)

    get_streams(song)

    if ext:
        extension = ext

    else:
        key = "a" if av == "audio" else "v"
        extension = song.urls[key]['ext']

    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
    # pylint: disable=E1103
    # Instance of 'bool' has no 'url' member (some types not inferable)

    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:
        get_streams(song)
        key = "a" if audio else "v"
        url = song.urls[key]["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.last_search_query = {}
            g.content = generate_songlist_display()

        elif saved and action == "view":
            g.last_search_query = {}
            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 g.browse_mode == "ytpl":

        if choice.isdigit():
            return plist(g.ytpls[int(choice) - 1]['link'])

        else:
            g.message = "Invalid playlist selection: %s" % c.y + choice + c.w
            g.content = generate_songlist_display()
            return

    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
        # when selecting a single item
        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.start()

        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.browse_mode = "normal"
        g.model.songs = g.active.songs
        g.message = F('current pl')

    g.content = generate_songlist_display(zeromsg=g.message)


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

    g.preloading.append(song.ytid)
    time.sleep(delay)
    video = Config.SHOW_VIDEO
    video = True if override in ("fullscreen", "window") else video
    video = False if override == "audio" else video

    try:
        get_streams(song, future=True)
        get_size(song, is_video=video)

    except (ValueError, AttributeError, IOError) as e:
        dbg(e)  # Fail silently on preload

    finally:
        g.preloading.remove(song.ytid)


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(c.w + "Stopping...                    " + c.w)
                time.sleep(2)
                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()
                    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()

                    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, mediatype="any"):
    """ Get filesize and metadata for all streams, return dict. """

    mbsize = lambda x: str(int(x / (1024 ** 2)))

    p = get_pafy(song)
    dldata = []
    text = " [Fetching stream info] >"
    streams = [x for x in p.allstreams]

    if mediatype == "audio":
        streams = [x for x in p.audiostreams]

    l = len(streams)
    for n, stream in enumerate(streams):
        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, p


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]

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

    elif not choice.strip():
        return False, False

    else:  # unrecognised input
        return False, "abort"


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

    # pylint: disable=R0914
    dl_data, p = get_dl_data(song)
    dl_text = gen_dl_text(dl_data, song, p)

    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)
    url2 = ext2 = None

    muxapp = has_exefile("avconv") or has_exefile("ffmpeg")
    if ext == "m4v" and muxapp:
        dl_data, p = get_dl_data(song, mediatype="audio")
        dl_text = gen_dl_text(dl_data, song, p)
        au_choices = "1" if len(dl_data) == 1 else "1-%s" % len(dl_data)
        footer = [F('-audio'), F('select mux') % au_choices]
        dl_text = tuple(dl_text[0:3]) + (footer,)
        aext = ("ogg", "m4a")
        model = [x['url'] for x in dl_data if x['ext'] in aext]
        ed = enumerate(dl_data)
        model = {str(n + 1): (x['url'], x['ext']) for n, x in ed}
        prompt = "Audio stream: "
        url2, ext2 = menu_prompt(model, prompt, *dl_text)

    return url, ext, url2, ext2


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

    hdr = []
    hdr.append("  %s%s%s" % (c.r, song.title, c.w))
    author = py2utf8_decode(p.author)
    hdr.append(c.r + "  Uploaded by " + author + c.w)
    hdr.append("  [" + fmt_time(song.length) + "]")
    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. """

    # This function needs refactoring!
    # pylint: disable=R0912
    # pylint: disable=R0914

    if g.browse_mode != "normal":
        g.message = "download must refer to a specific video item"
        g.content = generate_songlist_display()
        return

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

    if not best:

        try:
            # user prompt for download stream
            url, ext, url_au, ext_au = prompt_dl(song)

        except KeyboardInterrupt:
            g.message = c.r + "Download aborted!" + c.w
            g.content = generate_songlist_display()
            return

        if not url or ext_au == "abort":
            # abort on invalid stream selection
            g.content = generate_songlist_display()
            g.message = "%sNo download selected / invalid input%s" % (c.y, c.w)
            return

        else:
            # download user selected stream(s)
            filename = _make_fname(song, ext)
            args = (song, filename, url)
            kwargs = {}

            if url_au and ext_au:
                filename_au = _make_fname(song, ext_au)
                args_au = (song, filename_au, url_au)

    elif best:
        # set updownload without prompt
        url_au = None
        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:
        # perform download(s)
        dl_filenames = [args[1]]
        f = _download(*args, **kwargs)
        g.message = "Downloaded " + c.g + f + c.w

        if url_au:
            dl_filenames += [args_au[1]]
            _download(*args_au, **kwargs)

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

        try:
            for downloaded in dl_filenames:
                os.remove(downloaded)

        except IOError:
            pass

    if url_au:
        # multiplex
        muxapp = has_exefile("avconv") or has_exefile("ffmpeg")
        mux_cmd = "APP -i BISH -vcodec h264 -i BASH -acodec copy -map 0:v:0"
        mux_cmd += " -map 1:a:0 -threads 2 BOSH"
        mux_cmd = mux_cmd.split()
        mux_cmd[2], mux_cmd[6] = args[1], args_au[1]
        mux_cmd[0], mux_cmd[15] = muxapp, args[1][:-3] + "mp4"

        try:
            subprocess.call(mux_cmd)
            g.message = "Saved to :" + c.g + mux_cmd[15] + c.w
            os.remove(args[1])
            os.remove(args_au[1])

        except KeyboardInterrupt:
            g.message = "Audio/Video multiplex aborted!"

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

    glsq = g.last_search_query
    content = g.model.songs

    if "user" in g.last_search_query:
        function, query = usersearch, glsq['user']

    elif "term" in g.last_search_query:
        function, query = search, glsq['term']

    elif "playlists" in g.last_search_query:
        function, query = pl_search, glsq['playlists']
        content = g.ytpls

    elif "playlist" in g.last_search_query:
        function, query = plist, glsq['playlist']

    good = False

    if np == "n":
        if len(content) == g.max_results and glsq:
            g.current_page += 1
            good = True

    elif np == "p":
        if g.current_page > 1 and g.last_search_query:
            g.current_page -= 1
            good = True

    if good:
        function(query, g.current_page, splash=True)
        g.message += " : page %s" % g.current_page

    else:
        norp = "next" if np == "n" else "previous"
        g.message = "No %s items to display" % norp

    g.content = generate_songlist_display()


def user_more(num):
    """ Show more videos from user of vid num. """

    if g.browse_mode != "normal":
        g.message = "user uploads must refer to a specific video item"
        g.content = generate_songlist_display()
        return

    item = g.model.songs[int(num) - 1]
    p = get_pafy(item)
    user = p.username
    usersearch(user)


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

    if g.browse_mode == "ytpl":
        p = g.ytpls[int(num) - 1]

        # fetch the playlist item as it has more metadata
        yt_playlist = g.pafs.get(p['link'])

        if not yt_playlist:
            g.content = logo(col = c.g)
            g.message = "Fetching playlist info.."
            screen_update()
            yt_playlist = pafy.get_playlist(p['link'])
            g.pafs[p['link']] = yt_playlist

        ytpl_items = yt_playlist['items']
        ytpl_likes = yt_playlist.get('likes', 0)
        ytpl_dislikes = yt_playlist.get('dislikes', 0)
        ytpl_desc = yt_playlist.get('description', "")
        g.content = generate_songlist_display()

        created = time.strptime(p['created'], "%Y-%m-%dT%H:%M:%S.000Z")
        updated = time.strptime(p['updated'], "%Y-%m-%dT%H:%M:%S.000Z")
        out = c.ul + "Playlist Info" + c.w + "\n\n"
        out += p['title']
        out += "\n" + ytpl_desc
        out += ("\n\nAuthor     : " + p['author'])
        out += "\nSize       : " + str(p['size']) + " videos"
        out += "\nLikes      : " + str(ytpl_likes)
        out += "\nDislikes   : " + str(ytpl_dislikes)
        out += "\nCreated    : " + time.strftime("%x %X", created)
        out += "\nUpdated    : " + time.strftime("%x %X", updated)
        out += "\nID         : " + str(p['link'])
        out += ("\n\n%s[%sPress enter to go back%s]%s" % (c.y, c.w, c.y, c.w))
        g.content = out

    elif g.browse_mode == "normal":
        g.content = logo(c.b)
        screen_update()
        writestatus("Fetching video metadata..")
        item = (g.model.songs[int(num) - 1])
        get_streams(item)
        p = get_pafy(item)
        i = py2utf8_decode
        writestatus("Fetched")
        out = c.ul + "Video Info" + c.w + "\n\n"
        out += i(p.title or "")
        out += "\n" + (p.description or "")
        out += i("\n\nAuthor     : " + str(p.author))
        out += i("\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 play_url(url):
    """ Open and play a youtube video url. """

    g.browse_mode = "normal"
    yt_url(url)

    if len(g.model.songs) == 1:
        play("", "1")


def dl_url(url):
    """ Open and prompt for download of youtube video url. """

    g.browse_mode = "normal"
    yt_url(url)

    if len(g.model.songs) == 1:
        download("download", "1")


def yt_url(url):
    """ Acess a video by url. """

    try:
        p = pafy.new(url, basic=1, signature=0)

    except (IOError, ValueError) as e:
        g.message = c.r + str(e) + c.w
        g.content = g.content or generate_songlist_display(zeromsg=g.message)
        return

    g.browse_mode = "normal"
    v = Video(p.videoid, py2utf8_decode(p.title), p.length)
    g.model.songs = [v]
    g.content = generate_songlist_display()


def dump(un):
    """ Show entire playlist. """

    if g.last_search_query.get("playlist") and not un:
        plist(g.last_search_query['playlist'], dumps=True)

    elif g.last_search_query.get("playlist") and un:
        plist(g.last_search_query['playlist'], pagenum=1, dumps=False)

    else:
        un = "" if not un else un
        g.message = "%s%sdump%s may only be used on an open YouTube playlist"
        g.message = g.message % (c.y, un, c.w)
        g.content = generate_songlist_display()


def plist(parturl, pagenum=1, splash=True, dumps=False):
    """ Import playlist created on website. """

    if "playlist" in g.last_search_query and\
            parturl == g.last_search_query['playlist']:

        # go to pagenum
        s, e = (pagenum - 1) * g.max_results, pagenum * g.max_results

        if dumps:
            s, e = 0, 99999

        g.model.songs = g.ytpl['items'][s:e]
        g.content = generate_songlist_display()
        g.message = "Showing YouTube playlist: %s" % c.y + g.ytpl['name'] + c.w
        g.current_page = pagenum
        return

    if splash:
        g.content = logo(col=c.b)
        g.message = "Retreiving YouTube playlist"
        screen_update()

    yt_playlist = pafy.get_playlist(parturl)
    g.pafs[parturl] = yt_playlist
    ytpl_items = yt_playlist['items']
    ytpl_title = yt_playlist['title']
    g.content = generate_songlist_display()
    songs = []

    for item in ytpl_items:
        # Create Video object, appends to songs
        cur = Video(ytid=item['pafy'].videoid,
                    title=item['pafy'].title,
                    length=item['pafy'].length)
        songs.append(cur)

    if not ytpl_items:
        dbg("got unexpected data or no search results")
        return False

    g.last_search_query = {"playlist": parturl}
    g.browse_mode = "normal"
    g.ytpl = dict(name=ytpl_title, items=songs)
    g.current_page = 1
    g.model.songs = songs[:g.max_results]
    # preload first result url
    kwa = {"song": songs[0], "delay": 0}
    t = threading.Thread(target=preload, kwargs=kwa)
    t.start()

    g.content = generate_songlist_display()
    g.message = "Showing YouTube playlist %s" % (c.y + ytpl_title + c.w)


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}user <username>{1} - list YouTube uploads by <username>.
{2}pl <YouTube playlist url>{1} - Open YouTube playlist by url.
{2}pls <query>{1} - Search for YouTube playlists.
{2}url <YouTube url>{1} - Retrive specific YouTube video by url.

{2}u <number>{1} - show uploads by uploader of item <number>.
{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
{2}mpsyt playurl <url>{1} to play a YouTube video by url
{2}mpsyt dlurl <url>{1} to download a YouTube video by url
""".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.

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}dump{1} - to show entire contents of a YouTube playlist.
{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 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
    convert_playlist_to_v2()
    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*$',
        'dump': r'(un)?dump',
        '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)$',
        'plist': r'pl\s.*[^-_a-zA-Z0-9]([-_a-zA-Z0-9]{18,50})(?:(?:\&.*)|$)',
        'yt_url': r'url\s(.*[-_a-zA-Z0-9]{11}.*$)',
        'search': r'(?:search|\.|/)\s*(.{2,500})',
        'dl_url': r'dlurl\s(.*[-_a-zA-Z0-9]{11}.*$)',
        'play_pl': r'play\s{1,}(%s|\d+)$' % word,
        'download': r'(dv|da|d|dl|download)\s*(\d{1,4})$',
        'play_url': r'playurl\s(.*[-_a-zA-Z0-9]{11}.*$)',
        'nextprev': r'(n|p)$',
        'play_all': r'(%s{0,3})(?:\*|all)\s*(%s{0,3})$' % (rs, rs),
        'save_last': r'(save)\s*$',
        'pl_search': r'pls(?:earch)?\s(.*)$',
        'setconfig': r'set\s*(\w+)\s*"?([^"]*)"?\s*$',
        'show_help': r'(?:help|h)$',
        'usersearch': r'user\s?([^s].{2,})$',
        'user_more': r'u\s?([\d]{1,4})$',
        '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 = "> "

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

                if g.debug_mode:
                    globals()[func](*matches)

                else:

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

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

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

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