#!/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
from math import ceil

__version__ = "0.01.40"
__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

try:
    import readline
    readline.set_history_length(2000)
    has_readline = True

except ImportError:
    has_readline = 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", "")
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) == 1:
            ddir = defn[0].split("=")[1].replace('"', '')
            ddir = ddir.replace("$HOME", user_home).strip()

        else:
            ddir = DOWNLOAD_HOME if exists(DOWNLOAD_HOME) else user_home

    else:
        ddir = DOWNLOAD_HOME if exists(DOWNLOAD_HOME) else 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".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. """

    command_line = False
    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")
    READLINE_FILE = None
    playerargs_defaults = {
        "mpv": {"def": ['--really-quiet'],
                "title": "--title",
                "fs": "--fs",
                "novid": "--no-video",
                "ignidx": "--demuxer-lavf-o=fflags=+ignidx".split()
                },
        "mplayer": {"def": "-prefer-ipv4 -nolirc".split(),
                    "title": "-title",
                    "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 "--help" in args:

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

        sys.exit()

    g.command_line = "playurl" in args or "dlurl" in args
    g.blank_text = "" if g.command_line else g.blank_text


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

    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_readline():
    """ Enable readline for input history. """

    if g.command_line:
        return

    if has_readline:
        g.READLINE_FILE = os.path.join(get_config_dir(), "input_history")

        if os.path.exists(g.READLINE_FILE):
            readline.read_history_file(g.READLINE_FILE)
            dbg("Read history file")


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),
        'help topic': ("  Enter *help <topic>* for specific help:"),
        'help topic_': (c.y, 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.  """
    # pylint: disable=W1402

    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)
        updated = re.sub(r"(\d\d\D\d\d\D)20(\d\d)$", r"\1\2", 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 %-64s %-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(62, 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 generate_real_playerargs(song, override):
    """ Generate args for player command.

    Returns args and songdata status.

    """

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

    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():
        args.append(g.playerargs_defaults[Config.PLAYER]["title"])
        args.append(song.title)
        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"]

    return [Config.PLAYER] + args + [url], songdata


def playsong(song, failcount=0, override=False):
    """ Play song using config.PLAYER called with args config.PLAYERARGS."""

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

    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

    cmd, songdata = generate_real_playerargs(song, override)
    now = time.time()
    songdata = "%s; %s; %s Mb" % songdata
    writestatus(songdata)

    logging.info("playing %s (%s)", song.title, failcount)
    dbg("calling %s", cmd)

    try:

        if "mplayer" in Config.PLAYER:

            if "-really-quiet" in cmd:
                cmd.remove("-really-quiet")

            p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE,
                                 stderr=subprocess.STDOUT, bufsize=1)
            mplayer_status(p, songdata + ";", song.length)

        else:

            with open(os.devnull, "w") as devnull:
                subprocess.call(cmd, stderr=devnull)

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

    finally:
        try:
            p.terminate()  # make sure to kill mplayer if mpsyt crashes

        except (OSError, AttributeError, UnboundLocalError):
            pass

    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 mplayer_status(popen_object, prefix="", songlength=0):
    """ Capture time progress from player output. Write status line. """

    # A: 175.6
    re_mplayer = re.compile(r"A:\s*(?P<elapsed_s>\d+)\.\d\s*")
    last_displayed_line = None
    buff = ''

    while popen_object.poll() is None:
        char = popen_object.stdout.read(1).decode("utf-8")

        if char in '\r\n':
            m = re_mplayer.match(buff)

            if m:
                line = make_status_line(m, songlength)

                if line != last_displayed_line:
                    writestatus(prefix + (" " if prefix else "") + line)
                    last_displayed_line = line

            buff = ''

        else:
            buff += char


def make_status_line(match_object, songlength=0, progress_bar_size=30):
    """ Format progress line output.  """

    try:
        elapsed_s = int(match_object.group('elapsed_s') or '0')

    except ValueError:
        return ""

    display_s = elapsed_s
    display_h = display_m = 0

    if elapsed_s >= 60:
        display_m = display_s // 60
        display_s %= 60

        if display_m >= 100:
            display_h = display_m // 60
            display_m %= 60

    pct = (float(elapsed_s) / songlength * 100) if songlength else 0

    status_line = "%02i:%02i:%02i %s" % (
        display_h, display_m, display_s,
        ("[%.0f%%]" % pct).ljust(6)
    )

    progress = int(ceil(pct / 100 * progress_bar_size))
    status_line += " [%s]" % ("=" * (progress - 1) +
                              ">").ljust(progress_bar_size, ' ')

    return status_line


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 related_search(vitem, page=1, splash=True):
    """ Fetch uploads by a YouTube user. """

    query = generate_search_qs(vitem.ytid, page)
    del query['q']

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

    url = "https://gdata.youtube.com/feeds/api/videos/%s/related"
    url, t = url % vitem.ytid, vitem.title
    ttitle = t[:48].strip() + ".." if len(t) > 49 else t

    have_results = _search(url, ttitle, query)

    if have_results:
        g.message = "Videos related to %s%s%s" % (c.y, ttitle, c.w)
        g.last_opened = ""
        g.last_search_query = {"related": vitem}
        g.current_page = page
        g.content = generate_songlist_display()

    else:
        g.message = "Related to %s%s%s not found" % (c.y, vitem.ytid, 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 user_pls(user, page=1, splash=True):
    """ Retrieve use playlists. """

    user = {"is_user": True, "term": user}
    return pl_search(user, page=page, splash=splash)


def pl_search(term, page=1, splash=True, is_user=False):
    """ Search for YouTube playlists.

    term can be query str or dict indicating user playlist search.

    """

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

    if isinstance(term, dict):
        is_user = term["is_user"]
        term = term["term"]

    # generate url base on whether this is a user playlist search
    x = "/users/%s/playlists?" % term if is_user else "/playlists/snippets?"
    url = "https://gdata.youtube.com/feeds/api%s" % x
    prog = "user: " + term if is_user else term
    logging.info("playlist search for %s", prog)
    start = (page - 1) * g.max_results or 1
    qs = {"start-index": start,
          "max-results": g.max_results, "v": 2, 'alt': 'jsonc'}

    # modify query string based on whether this is a user playlst search.
    if not is_user:
        qs["q"] = term

    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 + prog + c.w
        screen_update()
        try:
            wpage = g.urlopen(url).read().decode("utf8")
            pldata = json.loads(wpage)
            playlists = get_pl_from_json(pldata)
        except HTTPError:
            playlists = None

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

    else:
        g.message = "No playlists found for: %s" % c.y + prog + 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_name(begin, items):
    """ Return the closest matching playlist name that starts with begin. """

    for name in sorted(items):
        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_name(name, g.userpl)
        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():
    """ 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_name(name, g.userpl)
            saved = g.userpl.get(name)

        if saved and action == "open":
            g.browse_mode = "normal"
            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.browse_mode = "normal"
            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 reset_terminal():
    """ Reset terminal control character and modes for non Win OS's. """

    if not mswin:
        subprocess.call(["tset", "-c"])


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)

            if not g.command_line:
                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...                          ")
                reset_terminal()
                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(c.w + "Stopping...                          ")
                reset_terminal()
                g.message = c.y + "Playback halted" + c.w
                break

    g.content = generate_songlist_display()


def show_help(choice):
    """ Print help message. """

    helps = {"download": ("playback dl listen watch show repeat playing"
                          "show_video shuffle playurl dlurl d da dv all *"
                          " play".split()
                          ),
             "invoke": "command commands mpsyt invocation".split(),
             "search": ("user userpl pl pls n p url rm mv sw edit move swap "
                        "editing result results remove swop".split()),
             "tips": ("undump dump -f -w -a adv advanced".split(" ")),
             "basic": ("r u i".split()),
             "config": ("set checkupdate colours colors ddir directory player "
                        "arguments args playerargs music search_music keys "
                        "status show_status show_video video configuration "
                        "fullscreen full screen folder player mpv mplayer"
                        " settings default reset configure audio".split()),
             "playlists": ("save rename delete move rm ls mv sw add vp open"
                           " view".split())
             }

    for topic, aliases in helps.items():

        if choice in aliases:
            choice = topic
            break

    choice = "menu" if not choice else choice
    out, all_help = "", g.helptext
    help_names = [x[0] for x in all_help]
    choice = _get_near_name(choice, help_names)

    if choice == "menu" or not choice in help_names:
        out += "  %sHelp Topics%s" % (c.ul, c.w)
        out += F('help topic', 2, 1)

        for x in all_help:
            out += ("\n%s     %-10s%s : %s" % (c.y, x[0], c.w, x[1]))

        out += "\n"
        g.content = out

    else:
        indent = lambda x: "\n  ".join(x.split("\n"))
        choice = help_names.index(choice)
        g.content = indent(all_help[choice][2])


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

    if has_readline:
        readline.write_history_file(g.READLINE_FILE)
        dbg("Saved history file")

    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.message = c.y + g.message + c.w
        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 "related" in g.last_search_query:
        function, query = related_search, glsq['related']

    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.message = c.y + g.message + c.w
        g.content = generate_songlist_display()
        return

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


def related(num):
    """ Show videos related to to vid num. """

    if g.browse_mode != "normal":
        g.message = "Related items must refer to a specific video item"
        g.message = c.y + g.message + c.w
        g.content = generate_songlist_display()
        return

    item = g.model.songs[int(num) - 1]
    related_search(item)


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_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, override):
    """ Open and play a youtube video url. """

    override = override if override else "_"
    g.browse_mode = "normal"
    yt_url(url, print_title=1)

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

    if g.command_line:
        sys.exit("")


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

    if g.command_line:
        sys.exit("")


def yt_url(url, print_title=0):
    """ 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]

    if not g.command_line:
        g.content = generate_songlist_display()

    if print_title:
        print(v.title)


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", "Basics", """
{0}Basic Usage{1}

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

Then, when results are shown:

    {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 video <number>
    {2}d <number>{1} - download video <number>
    {2}r <number>{1} - show videos related to video <number>.
    {2}u <number>{1} - show videos uploaded by uploader of video <number>.
""".format(c.ul, c.w, c.y, c.r)),

    ("search", "Searching and Editing Results", """
{0}Searching and Editing Results{1}

{2}set search_music false{1} - search all YouTube categories.
{2}set search_music true{1}  - search only YouTube music category.

{2}/<query>{1} or {2}.<query>{1} to search for videos. e.g., {2}/daft punk{1}
{2}//<query>{1} or {2}..<query>{1} - search for YouTube playlists. e.g., \
{2}//80's music{1}
{2}n{1} and {2}p{1} - continue search to next/previous pages.

{2}user <username>{1} - list YouTube uploads by <username>.
{2}userpl <username>{1} - list YouTube playlists created by <username>.
{2}pl <YouTube playlist url>{1} - Open YouTube playlist by url.
{2}url <YouTube url>{1} - Retrive specific YouTube video by url.

{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>.
{2}r <number>{1} - show videos related to video <number>.
{2}u <number>{1} - show videos uploaded by uploader of video <number>.
""".format(c.ul, c.w, c.y, c.r)),

    ("download", "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}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}dlurl <url or id>{1} download a YouTube video by url or video id
{2}playurl <url or id>{1} play a YouTube video by url or id

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

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

    ("playlists", "Using Local Playlists", """
{0}Using Local 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.
     (<playlist> will be created if it doesn't already exist)

{2}vp{1} - view current playlist.
{2}ls{1} - list saved playlists.
{2}mv <old name or ID> <new name>{1} - rename a playlist.
{2}rmp <playlist_name or ID>{1} - delete a playlist from disk.

{2}open <name or ID>{1} - open a saved playlist as the current playlist.
{2}play <name or ID>{1} - play a saved playlist directly.
{2}view <name or ID>{1} - view a playlist (current playlist left intact)
{2}save{1} or {2}save <name>{1} - save the displayed items as a playlist.

{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.y, c.r)),

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

All commands that can be used in mpsyt can be invoked from the command line
where this makes sense, so excluding commands that act on specific results.

For example;

  {2}mpsyt dlurl <url or id>{1} to download a YouTube video by url or id
  {2}mpsyt playurl <url or id>{1} to play a YouTube video by url or id
  {2}mpsyt /mozart{1} to search
  {2}mpsyt //best songs of 2010{1} for a playlist search
  {2}mpsyt play <playlist name or ID>{1} to play a saved playlist
  {2}mpsyt ls{1} to list saved playlists

When invoking with playurl or dlurl, a less interactive session will be used
and mpsyt will exit when done.
""".format(c.ul, c.w, c.y, c.r)),

    ("config", "Configuration Options", """
{0}Configuration{1}

{2}set{1} - view current configuration

{2}set all default{1} - restore default settings

{2}set checkupdate true|false{1} - if true, checks for updates on exit
{2}set colours true|false{1} - use colours in display output
{2}set ddir <download direcory>{1} - sets where files are saved
{2}set fullscreen true|false{1} - if true, video output is full screen
{2}set player <player app>{1} - use <player app> for playback
{2}set playerargs <args>{1} - use specified arguments to player
{2}set search_music true|false{1} - search only music or all categories
{2}set show_mplayer_keys true|false{1} - show keyboard help for mplayer and mpv
{2}set show_status true|false{1} - shows status messages
{2}set show_video true|false{1} - if true, shows video output in player

[Tip: you can use {2}1{1} and {2}0{1} in place of true and false]
""".format(c.ul, c.w, c.y, c.r)),

    ("tips", "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
Reversed ranges also work. eg., 5-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.

{2}dump{1} - to show entire contents of an opened YouTube playlist.
       (useful for playing or saving entire playlists, user {2}undump{1} to \
undo)
{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.y, c.r))
]


def matchfunction(funcname, regex, userinput):
    """ Match userinput against regex.

    Call funcname, return True if matches.

    """

    if regex.match(userinput):
        matches = regex.match(userinput).groups()

        if g.debug_mode:
            dbg("input: %s", userinput)
            dbg("function call: %s", funcname)
            dbg("regx matches: %s", matches)
            globals()[funcname](*matches)

        else:

            try:
                globals()[funcname](*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)

        return True


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

    if not g.command_line:
        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
    arg_inp = " ".join(sys.argv[1:])

    # input types
    #yt = r'[^-_a-zA-Z0-9]?([-_a-zA-Z0-9]{11})[^-_a-zA-Z0-9]?
    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*([^./].{1,500})',
        'dl_url': r'dlurl\s(.*[-_a-zA-Z0-9]{11}.*$)',
        'play_pl': r'play\s+(%s|\d+)$' % word,
        'related': r'r\s?(\d{1,4})$',
        'download': r'(dv|da|d|dl|download)\s*(\d{1,4})$',
        'play_url': r'playurl\s(.*[-_a-zA-Z0-9]{11}[^\s]*)(\s-(?:f|a|w))?$',
        'nextprev': r'(n|p)$',
        'play_all': r'(%s{0,3})(?:\*|all)\s*(%s{0,3})$' % (rs, rs),
        'user_pls': r'u(?:ser)?pl\s(.*)$',
        'save_last': r'save\s*$',
        'pl_search': r'(?:\.\.|\/\/|pls(?:earch)?\s)\s*(.*)$',
        'setconfig': r'set\s*(\w+)\s*"?([^"]*)"?\s*$',
        'show_help': r'(?:help|h)(?:\s+(-?\w+)\s*)?$',
        '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
        #'play_url': r'(.*[-_a-zA-Z0-9]{11}[^\s]*)(\s-(?:f|a|w))?$'
    }

    # compile regexp's
    regx = {name: re.compile(val, re.UNICODE) for name, val in regx.items()}
    prompt = "> "

    while True:
        try:
            # get user input
            userinput = arg_inp or compat_input(prompt)
            userinput = userinput.strip()

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

        arg_inp = None

        for k, v in regx.items():
            if matchfunction(k, v, userinput):
                break

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

            if g.command_line:
                g.content = ""

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

            elif userinput and g.command_line:
                sys.exit("Bad syntax")

        screen_update()

if "--debug" in sys.argv:
    print(get_version_info())
    sys.argv = [_x for _x in sys.argv if not "--debug" in _x]
    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)

dbg = logging.debug

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