mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
Move human formatting functions under beets.util.units
This commit is contained in:
parent
8937978d5f
commit
9d088ab69f
7 changed files with 117 additions and 110 deletions
|
|
@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
|
|||
|
||||
import beets
|
||||
from beets import util
|
||||
from beets.util.units import human_seconds_short, raw_seconds_short
|
||||
|
||||
from . import query
|
||||
|
||||
|
|
@ -437,14 +438,14 @@ class DurationType(Float):
|
|||
|
||||
def format(self, value):
|
||||
if not beets.config["format_raw_length"].get(bool):
|
||||
return util.human_seconds_short(value or 0.0)
|
||||
return human_seconds_short(value or 0.0)
|
||||
else:
|
||||
return value
|
||||
|
||||
def parse(self, string):
|
||||
try:
|
||||
# Try to format back hh:ss to seconds.
|
||||
return util.raw_seconds_short(string)
|
||||
return raw_seconds_short(string)
|
||||
except ValueError:
|
||||
# Fall back to a plain float.
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -435,48 +435,6 @@ def input_select_objects(prompt, objs, rep, prompt_all=None):
|
|||
return []
|
||||
|
||||
|
||||
# Human output formatting.
|
||||
|
||||
|
||||
def human_bytes(size):
|
||||
"""Formats size, a number of bytes, in a human-readable way."""
|
||||
powers = ["", "K", "M", "G", "T", "P", "E", "Z", "Y", "H"]
|
||||
unit = "B"
|
||||
for power in powers:
|
||||
if size < 1024:
|
||||
return f"{size:3.1f} {power}{unit}"
|
||||
size /= 1024.0
|
||||
unit = "iB"
|
||||
return "big"
|
||||
|
||||
|
||||
def human_seconds(interval):
|
||||
"""Formats interval, a number of seconds, as a human-readable time
|
||||
interval using English words.
|
||||
"""
|
||||
units = [
|
||||
(1, "second"),
|
||||
(60, "minute"),
|
||||
(60, "hour"),
|
||||
(24, "day"),
|
||||
(7, "week"),
|
||||
(52, "year"),
|
||||
(10, "decade"),
|
||||
]
|
||||
for i in range(len(units) - 1):
|
||||
increment, suffix = units[i]
|
||||
next_increment, _ = units[i + 1]
|
||||
interval /= float(increment)
|
||||
if interval < next_increment:
|
||||
break
|
||||
else:
|
||||
# Last unit.
|
||||
increment, suffix = units[-1]
|
||||
interval /= float(increment)
|
||||
|
||||
return f"{interval:3.1f} {suffix}s"
|
||||
|
||||
|
||||
# Colorization.
|
||||
|
||||
# ANSI terminal colorization code heavily inspired by pygments:
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ from beets.util import (
|
|||
normpath,
|
||||
syspath,
|
||||
)
|
||||
from beets.util.units import human_bytes, human_seconds, human_seconds_short
|
||||
|
||||
from . import _store_dict
|
||||
|
||||
|
|
@ -541,8 +542,8 @@ class ChangeRepresentation:
|
|||
cur_length0 = item.length if item.length else 0
|
||||
new_length0 = track_info.length if track_info.length else 0
|
||||
# format into string
|
||||
cur_length = f"({util.human_seconds_short(cur_length0)})"
|
||||
new_length = f"({util.human_seconds_short(new_length0)})"
|
||||
cur_length = f"({human_seconds_short(cur_length0)})"
|
||||
new_length = f"({human_seconds_short(new_length0)})"
|
||||
# colorize
|
||||
lhs_length = ui.colorize(highlight_color, cur_length)
|
||||
rhs_length = ui.colorize(highlight_color, new_length)
|
||||
|
|
@ -706,14 +707,14 @@ class AlbumChange(ChangeRepresentation):
|
|||
for track_info in self.match.extra_tracks:
|
||||
line = f" ! {track_info.title} (#{self.format_index(track_info)})"
|
||||
if track_info.length:
|
||||
line += f" ({util.human_seconds_short(track_info.length)})"
|
||||
line += f" ({human_seconds_short(track_info.length)})"
|
||||
print_(ui.colorize("text_warning", line))
|
||||
if self.match.extra_items:
|
||||
print_(f"Unmatched tracks ({len(self.match.extra_items)}):")
|
||||
for item in self.match.extra_items:
|
||||
line = " ! {} (#{})".format(item.title, self.format_index(item))
|
||||
if item.length:
|
||||
line += " ({})".format(util.human_seconds_short(item.length))
|
||||
line += " ({})".format(human_seconds_short(item.length))
|
||||
print_(ui.colorize("text_warning", line))
|
||||
|
||||
|
||||
|
|
@ -795,8 +796,8 @@ def summarize_items(items, singleton):
|
|||
round(int(items[0].samplerate) / 1000, 1), items[0].bitdepth
|
||||
)
|
||||
summary_parts.append(sample_bits)
|
||||
summary_parts.append(util.human_seconds_short(total_duration))
|
||||
summary_parts.append(ui.human_bytes(total_filesize))
|
||||
summary_parts.append(human_seconds_short(total_duration))
|
||||
summary_parts.append(human_bytes(total_filesize))
|
||||
|
||||
return ", ".join(summary_parts)
|
||||
|
||||
|
|
@ -1906,7 +1907,7 @@ def show_stats(lib, query, exact):
|
|||
if item.album_id:
|
||||
albums.add(item.album_id)
|
||||
|
||||
size_str = "" + ui.human_bytes(total_size)
|
||||
size_str = "" + human_bytes(total_size)
|
||||
if exact:
|
||||
size_str += f" ({total_size} bytes)"
|
||||
|
||||
|
|
@ -1918,7 +1919,7 @@ Artists: {}
|
|||
Albums: {}
|
||||
Album artists: {}""".format(
|
||||
total_items,
|
||||
ui.human_seconds(total_time),
|
||||
human_seconds(total_time),
|
||||
f" ({total_time:.2f} seconds)" if exact else "",
|
||||
"Total size" if exact else "Approximate total size",
|
||||
size_str,
|
||||
|
|
|
|||
|
|
@ -1019,27 +1019,6 @@ def case_sensitive(path: bytes) -> bool:
|
|||
return not os.path.samefile(lower_sys, upper_sys)
|
||||
|
||||
|
||||
def raw_seconds_short(string: str) -> float:
|
||||
"""Formats a human-readable M:SS string as a float (number of seconds).
|
||||
|
||||
Raises ValueError if the conversion cannot take place due to `string` not
|
||||
being in the right format.
|
||||
"""
|
||||
match = re.match(r"^(\d+):([0-5]\d)$", string)
|
||||
if not match:
|
||||
raise ValueError("String not in M:SS format")
|
||||
minutes, seconds = map(int, match.groups())
|
||||
return float(minutes * 60 + seconds)
|
||||
|
||||
|
||||
def human_seconds_short(interval):
|
||||
"""Formats a number of seconds as a short human-readable M:SS
|
||||
string.
|
||||
"""
|
||||
interval = int(interval)
|
||||
return "%i:%02i" % (interval // 60, interval % 60)
|
||||
|
||||
|
||||
def asciify_path(path: str, sep_replace: str) -> str:
|
||||
"""Decodes all unicode characters in a path into ASCII equivalents.
|
||||
|
||||
|
|
|
|||
61
beets/util/units.py
Normal file
61
beets/util/units.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import re
|
||||
|
||||
|
||||
def raw_seconds_short(string: str) -> float:
|
||||
"""Formats a human-readable M:SS string as a float (number of seconds).
|
||||
|
||||
Raises ValueError if the conversion cannot take place due to `string` not
|
||||
being in the right format.
|
||||
"""
|
||||
match = re.match(r"^(\d+):([0-5]\d)$", string)
|
||||
if not match:
|
||||
raise ValueError("String not in M:SS format")
|
||||
minutes, seconds = map(int, match.groups())
|
||||
return float(minutes * 60 + seconds)
|
||||
|
||||
|
||||
def human_seconds_short(interval):
|
||||
"""Formats a number of seconds as a short human-readable M:SS
|
||||
string.
|
||||
"""
|
||||
interval = int(interval)
|
||||
return "%i:%02i" % (interval // 60, interval % 60)
|
||||
|
||||
|
||||
def human_bytes(size):
|
||||
"""Formats size, a number of bytes, in a human-readable way."""
|
||||
powers = ["", "K", "M", "G", "T", "P", "E", "Z", "Y", "H"]
|
||||
unit = "B"
|
||||
for power in powers:
|
||||
if size < 1024:
|
||||
return f"{size:3.1f} {power}{unit}"
|
||||
size /= 1024.0
|
||||
unit = "iB"
|
||||
return "big"
|
||||
|
||||
|
||||
def human_seconds(interval):
|
||||
"""Formats interval, a number of seconds, as a human-readable time
|
||||
interval using English words.
|
||||
"""
|
||||
units = [
|
||||
(1, "second"),
|
||||
(60, "minute"),
|
||||
(60, "hour"),
|
||||
(24, "day"),
|
||||
(7, "week"),
|
||||
(52, "year"),
|
||||
(10, "decade"),
|
||||
]
|
||||
for i in range(len(units) - 1):
|
||||
increment, suffix = units[i]
|
||||
next_increment, _ = units[i + 1]
|
||||
interval /= float(increment)
|
||||
if interval < next_increment:
|
||||
break
|
||||
else:
|
||||
# Last unit.
|
||||
increment, suffix = units[-1]
|
||||
interval /= float(increment)
|
||||
|
||||
return f"{interval:3.1f} {suffix}s"
|
||||
|
|
@ -21,7 +21,7 @@ from random import random
|
|||
|
||||
from beets import config, ui
|
||||
from beets.test import _common
|
||||
from beets.test.helper import BeetsTestCase, ItemInDBTestCase, control_stdin
|
||||
from beets.test.helper import BeetsTestCase, control_stdin
|
||||
|
||||
|
||||
class InputMethodsTest(BeetsTestCase):
|
||||
|
|
@ -88,42 +88,6 @@ class InputMethodsTest(BeetsTestCase):
|
|||
assert items == ["1", "3"]
|
||||
|
||||
|
||||
class InitTest(ItemInDBTestCase):
|
||||
def test_human_bytes(self):
|
||||
tests = [
|
||||
(0, "0.0 B"),
|
||||
(30, "30.0 B"),
|
||||
(pow(2, 10), "1.0 KiB"),
|
||||
(pow(2, 20), "1.0 MiB"),
|
||||
(pow(2, 30), "1.0 GiB"),
|
||||
(pow(2, 40), "1.0 TiB"),
|
||||
(pow(2, 50), "1.0 PiB"),
|
||||
(pow(2, 60), "1.0 EiB"),
|
||||
(pow(2, 70), "1.0 ZiB"),
|
||||
(pow(2, 80), "1.0 YiB"),
|
||||
(pow(2, 90), "1.0 HiB"),
|
||||
(pow(2, 100), "big"),
|
||||
]
|
||||
for i, h in tests:
|
||||
assert h == ui.human_bytes(i)
|
||||
|
||||
def test_human_seconds(self):
|
||||
tests = [
|
||||
(0, "0.0 seconds"),
|
||||
(30, "30.0 seconds"),
|
||||
(60, "1.0 minutes"),
|
||||
(90, "1.5 minutes"),
|
||||
(125, "2.1 minutes"),
|
||||
(3600, "1.0 hours"),
|
||||
(86400, "1.0 days"),
|
||||
(604800, "1.0 weeks"),
|
||||
(31449600, "1.0 years"),
|
||||
(314496000, "1.0 decades"),
|
||||
]
|
||||
for i, h in tests:
|
||||
assert h == ui.human_seconds(i)
|
||||
|
||||
|
||||
class ParentalDirCreation(BeetsTestCase):
|
||||
def test_create_yes(self):
|
||||
non_exist_path = _common.os.fsdecode(
|
||||
|
|
|
|||
43
test/util/test_units.py
Normal file
43
test/util/test_units.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import pytest
|
||||
|
||||
from beets.util.units import human_bytes, human_seconds
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_bytes,expected",
|
||||
[
|
||||
(0, "0.0 B"),
|
||||
(30, "30.0 B"),
|
||||
(pow(2, 10), "1.0 KiB"),
|
||||
(pow(2, 20), "1.0 MiB"),
|
||||
(pow(2, 30), "1.0 GiB"),
|
||||
(pow(2, 40), "1.0 TiB"),
|
||||
(pow(2, 50), "1.0 PiB"),
|
||||
(pow(2, 60), "1.0 EiB"),
|
||||
(pow(2, 70), "1.0 ZiB"),
|
||||
(pow(2, 80), "1.0 YiB"),
|
||||
(pow(2, 90), "1.0 HiB"),
|
||||
(pow(2, 100), "big"),
|
||||
],
|
||||
)
|
||||
def test_human_bytes(input_bytes, expected):
|
||||
assert human_bytes(input_bytes) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_seconds,expected",
|
||||
[
|
||||
(0, "0.0 seconds"),
|
||||
(30, "30.0 seconds"),
|
||||
(60, "1.0 minutes"),
|
||||
(90, "1.5 minutes"),
|
||||
(125, "2.1 minutes"),
|
||||
(3600, "1.0 hours"),
|
||||
(86400, "1.0 days"),
|
||||
(604800, "1.0 weeks"),
|
||||
(31449600, "1.0 years"),
|
||||
(314496000, "1.0 decades"),
|
||||
],
|
||||
)
|
||||
def test_human_seconds(input_seconds, expected):
|
||||
assert human_seconds(input_seconds) == expected
|
||||
Loading…
Reference in a new issue