Skip to content

util.py

IterSlicer

Class that takes an iterable and allows slicing, loading from the iterable as needed.

Source code in mps_youtube/util.py
class IterSlicer():
    """ Class that takes an iterable and allows slicing,
        loading from the iterable as needed."""

    def __init__(self, iterable, length=None):
        self.ilist = []
        self.iterable = iter(iterable)
        self.length = length
        if length is None:
            try:
                self.length = len(iterable)
            except TypeError:
                pass

    def __getitem__(self, sliced):
        if isinstance(sliced, slice):
            stop = sliced.stop
        else:
            stop = sliced
        # To get the last item in an iterable, must iterate over all items
        if (stop is None) or (stop < 0):
            stop = None
        while (stop is None) or (stop > len(self.ilist) - 1):
            try:
                self.ilist.append(next(self.iterable))
            except StopIteration:
                break

        return self.ilist[sliced]

    def __len__(self):
        if self.length is None:
            self.length = len(self[:])
        return self.length

XYTuple (tuple)

XYTuple(width, height, max_results)

__getnewargs__(self) special

Return self as a plain tuple. Used by copy and pickle.

Source code in mps_youtube/util.py
def __getnewargs__(self):
    'Return self as a plain tuple.  Used by copy and pickle.'
    return _tuple(self)

__new__(_cls, width, height, max_results) special staticmethod

Create new instance of XYTuple(width, height, max_results)

__repr__(self) special

Return a nicely formatted representation string

Source code in mps_youtube/util.py
def __repr__(self):
    'Return a nicely formatted representation string'
    return self.__class__.__name__ + repr_fmt % self

F(key, nb=0, na=0, textlib=None)

Format text.

:param nb: newline before :type nb: int :param na: newline after :type na: int :param textlib: the dictionary to use (defaults to g.text if not given) :type textlib: dict :returns: A string, potentially containing one or more %s :rtype: str

Source code in mps_youtube/util.py
def F(key, nb=0, na=0, textlib=None):
    """Format text.

    :param nb: newline before
    :type nb: int
    :param na: newline after
    :type na: int
    :param textlib: the dictionary to use (defaults to g.text if not given)
    :type textlib: dict
    :returns: A string, potentially containing one or more %s
    :rtype: str
    """
    textlib = textlib or g.text

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

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

    text = text.replace("&&", "%s")

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

correct_truncate(text, max_len)

Truncate a string taking into account East Asian width chars.

Source code in mps_youtube/util.py
def correct_truncate(text, max_len):
    """ Truncate a string taking into account East Asian width chars."""
    str_len, out = 0, ''

    for c in text:
        str_len += real_len(c)

        if str_len <= max_len:
            out += c

        else:
            break

    return out

dbg(*args)

Emit a debug message.

Source code in mps_youtube/util.py
def dbg(*args):
    """Emit a debug message."""
    # Uses xenc to deal with UnicodeEncodeError when writing to terminal
    logging.debug(*(xenc(i) for i in args))

fmt_time(seconds)

Format number of seconds to %H:%M:%S.

Source code in mps_youtube/util.py
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

get_near_name(begin, items)

Return the closest matching playlist name that starts with begin.

Source code in mps_youtube/util.py
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()):
            return name
    return begin

get_pafy(item, force=False, callback=None)

Get pafy object for an item.

:param item: video to retrieve :type item: :class:mps_youtube.playlist.Video or str :param force: ignore cache and retrieve anyway :type force: bool :param callback: callpack to pass to pafy :type callback: func :rtype: Pafy

Source code in mps_youtube/util.py
def get_pafy(item, force=False, callback=None):
    """
    Get pafy object for an item.

    :param item: video to retrieve
    :type item: :class:`mps_youtube.playlist.Video` or str
    :param force: ignore cache and retrieve anyway
    :type force: bool
    :param callback: callpack to pass to pafy
    :type callback: func
    :rtype: Pafy
    """

    if isinstance(item, Video):
        ytid = item.ytid
    else:
        ytid = item
    callback_fn = callback or (lambda x: None)
    cached = g.pafs.get(ytid)

    if not force and cached and cached.expiry > time.time():
        dbg("get pafy cache hit for %s", cached.title)
        cached.fresh = False
        return cached

    else:

        try:
            p = None#pafy.new(ytid, callback=callback_fn)

        except IOError as e:

            if "pafy" in str(e):
                dbg(c.p + "retrying failed pafy get: " + ytid + c.w)
                p = None#pafy.new(ytid, callback=callback)

            else:
                raise

        g.pafs[ytid] = p
        p.fresh = True
        thread = "preload: " if not callback else ""
        dbg("%s%sgot new pafy object: %s%s" % (c.y, thread, p.title[:26], c.w))
        dbg("%s%sgot new pafy object: %s%s" % (c.y, thread, p.videoid, c.w))
        return p

getxy()

Get terminal size, terminal width and max-results.

:rtype: :class:XYTuple

Source code in mps_youtube/util.py
def getxy():
    """
    Get terminal size, terminal width and max-results.

    :rtype: :class:`XYTuple`
    """
    # Import here to avoid circular dependency
    from . import config
    if g.detectable_size:
        x, y = terminalsize.get_terminal_size()
        max_results = y - 4 if y < 54 else 50
        max_results = 1 if y <= 5 else max_results

    else:
        x, max_results = config.CONSOLE_WIDTH.get, config.MAX_RESULTS.get
        y = max_results + 4

    return XYTuple(x, y, max_results)

has_exefile(filename)

Check whether file exists in path and is executable.

:param filename: name of executable :type filename: str :returns: Path to file or False if not found :rtype: str or False

Source code in mps_youtube/util.py
def has_exefile(filename):
    """ Check whether file exists in path and is executable.

    :param filename: name of executable
    :type filename: str
    :returns: Path to file or False if not found
    :rtype: str or False
    """
    paths = [os.getcwd()] + os.environ.get("PATH", '').split(os.pathsep)
    paths = [i for i in paths if i]
    dbg("searching path for %s", filename)

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

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

    return False

is_known_player(player)

Return true if the set player is known.

Source code in mps_youtube/util.py
def is_known_player(player):
    """ Return true if the set player is known. """
    for allowed_player in g.playerargs_defaults:
        regex = r'(?:\b%s($|\.[a-zA-Z0-9]+$))' % re.escape(allowed_player)
        match = re.search(regex, player)

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

        if match:
            return allowed_player

    return None

list_update(item, lst, remove=False)

Add or remove item from list, checking first to avoid exceptions.

Source code in mps_youtube/util.py
def list_update(item, lst, remove=False):
    """ Add or remove item from list, checking first to avoid exceptions. """
    if not remove and item not in lst:
        lst.append(item)

    elif remove and item in lst:
        lst.remove(item)

mswinfn(filename)

Fix filename for Windows.

Source code in mps_youtube/util.py
def mswinfn(filename):
    """ Fix filename for Windows. """
    if mswin:
        filename = utf8_replace(filename) if not_utf8_environment else filename
        allowed = re.compile(r'[^\\/?*$\'"%&:<>|]')
        filename = "".join(x if allowed.match(x) else "_" for x in filename)

    return filename

number_string_to_list(text)

Parses comma separated lists

Source code in mps_youtube/util.py
def number_string_to_list(text):
    """ Parses comma separated lists """
    text = [x.strip() for x in text.split(",")]
    vals = []
    for line in text:
        k = line
        if "-" in line:
            separated = [int(x.strip()) for x in k.split("-")]
            for number in list(range(separated[0]-1, separated[1])):
                vals.append(number)
        else:
            vals.append(k)

    return [int(x) - 1 for x in vals]

parse_multi(choice, end=None)

Handle ranges like 5-9, 9-5, 5- and -5 with optional repetitions number [n]

eg. 2-4[2] is the same as 2 3 4 2 3 4 and 3[4] is 3 3 3 3

Return list of ints.

Source code in mps_youtube/util.py
def parse_multi(choice, end=None):
    """
    Handle ranges like 5-9, 9-5, 5- and -5 with optional repetitions number [n]

    eg. 2-4[2] is the same as 2 3 4 2 3 4 and 3[4] is 3 3 3 3

    Return list of ints.

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

    for x, nreps in items:
        # nreps is in the inclusive range [1,100]
        nreps = min(int(nreps), 100) if nreps else 1
        for _ in range(nreps):

            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

parse_video_length(duration)

Converts HH:MM:SS to a single integer .i.e. total number of seconds

Source code in mps_youtube/util.py
def parse_video_length(duration):
    '''
    Converts HH:MM:SS to a single integer .i.e. total number of seconds
    '''
    if duration:
        duration_tokens = duration.split(":")
        if len(duration_tokens) == 2:
            return int(duration_tokens[0]) * 60 + int(duration_tokens[1])
        else:
            return int(duration_tokens[0]) * 3600 + int(duration_tokens[1]) * 60 + int(duration_tokens[2])
    else:
        return 10

real_len(u, alt=False)

Try to determine width of strings displayed with monospace font.

Source code in mps_youtube/util.py
def real_len(u, alt=False):
    """ Try to determine width of strings displayed with monospace font. """
    if not isinstance(u, str):
        u = u.decode("utf8")

    u = xenc(u) # Handle replacements of unsuported characters

    ueaw = unicodedata.east_asian_width

    if alt:
        # widths = dict(W=2, F=2, A=1, N=0.75, H=0.5)  # original
        widths = dict(N=.75, Na=1, W=2, F=2, A=1)

    else:
        widths = dict(W=2, F=2, A=1, N=1, H=0.5)

    return int(round(sum(widths.get(ueaw(char), 1) for char in u)))

sanitize_filename(filename, ignore_slashes=False)

Sanitize filename

Source code in mps_youtube/util.py
def sanitize_filename(filename, ignore_slashes=False):
    """ Sanitize filename """
    if not ignore_slashes:
        filename = filename.replace('/', '-')
    if macos:
        filename = filename.replace(':', '_')
    if mswin:
        filename = utf8_replace(filename) if not_utf8_environment else filename
        allowed = re.compile(r'[^\\?*$\'"%&:<>|]')
        filename = "".join(x if allowed.match(x) else "_" for x in filename)

    return filename

set_window_title(title)

Set terminal window title.

Source code in mps_youtube/util.py
def set_window_title(title):
    """ Set terminal window title. """
    if mswin:
        ctypes.windll.kernel32.SetConsoleTitleW(xenc(title))
    else:
        sys.stdout.write(xenc('\x1b]2;' + title + '\x07'))

uea_pad(num, t, direction='<', notrunc=False)

Right pad with spaces taking into account East Asian width chars.

Source code in mps_youtube/util.py
def uea_pad(num, t, direction="<", notrunc=False):
    """ Right pad with spaces taking into account East Asian width chars. """
    direction = direction.strip() or "<"

    t = ' '.join(str(t).split('\n'))

    # TODO: Find better way of dealing with this?
    if num <= 0:
        return ''

    if not notrunc:
        # Truncate to max of num characters
        t = correct_truncate(t, num)

    if real_len(t) < num:
        spaces = num - real_len(t)

        if direction == "<":
            t = t + (" " * spaces)

        elif direction == ">":
            t = (" " * spaces) + t

        elif direction == "^":
            right = False

            while real_len(t) < num:
                t = t + " " if right else " " + t
                right = not right

    return t

utf8_replace(txt)

Replace unsupported characters in unicode string.

:param txt: text to filter :type txt: str :returns: Unicode text without any characters unsupported by locale :rtype: str

Source code in mps_youtube/util.py
def utf8_replace(txt):
    """
    Replace unsupported characters in unicode string.

    :param txt: text to filter
    :type txt: str
    :returns: Unicode text without any characters unsupported by locale
    :rtype: str
    """
    sse = sys.stdout.encoding
    txt = str(txt)
    txt = txt.encode(sse, "replace").decode(sse)
    return txt

xenc(stuff)

Replace unsupported characters.

Source code in mps_youtube/util.py
def xenc(stuff):
    """ Replace unsupported characters. """
    return utf8_replace(stuff) if not_utf8_environment else stuff

xprint(stuff, end=None)

Compatible print.

Source code in mps_youtube/util.py
def xprint(stuff, end=None):
    """ Compatible print. """
    print(xenc(stuff), end=end)

yt_datetime(yt_date_time)

Return a time object, locale formated date string and locale formatted time string.

Source code in mps_youtube/util.py
def yt_datetime(yt_date_time):
    """ Return a time object, locale formated date string and locale formatted time string. """
    if yt_date_time is None:
        return ['Unknown', 'Unknown', 'Unknown']
    time_obj = time.strptime(yt_date_time, "%Y-%m-%dT%H:%M:%SZ")
    locale_date = time.strftime("%x", time_obj)
    locale_time = time.strftime("%X", time_obj)
    # strip first two digits of four digit year
    short_date = re.sub(r"(\d\d\D\d\d\D)20(\d\d)$", r"\1\2", locale_date)
    return time_obj, short_date, locale_time

yt_datetime_local(yt_date_time)

Return a datetime object, locale converted and formated date string and locale converted and formatted time string.

Source code in mps_youtube/util.py
def yt_datetime_local(yt_date_time):
    """ Return a datetime object, locale converted and formated date string and locale converted and formatted time string. """
    datetime_obj = datetime.strptime(yt_date_time, "%Y-%m-%dT%H:%M:%SZ")
    datetime_obj = utc2local(datetime_obj)
    locale_date = datetime_obj.strftime("%x")
    locale_time = datetime_obj.strftime("%X")
    # strip first two digits of four digit year
    short_date = re.sub(r"(\d\d\D\d\d\D)20(\d\d)$", r"\1\2", locale_date)
    return datetime_obj, short_date, locale_time