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