mirror of
https://github.com/beetbox/beets.git
synced 2025-12-08 01:23:09 +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
|
import beets
|
||||||
from beets import util
|
from beets import util
|
||||||
|
from beets.util.units import human_seconds_short, raw_seconds_short
|
||||||
|
|
||||||
from . import query
|
from . import query
|
||||||
|
|
||||||
|
|
@ -437,14 +438,14 @@ class DurationType(Float):
|
||||||
|
|
||||||
def format(self, value):
|
def format(self, value):
|
||||||
if not beets.config["format_raw_length"].get(bool):
|
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:
|
else:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def parse(self, string):
|
def parse(self, string):
|
||||||
try:
|
try:
|
||||||
# Try to format back hh:ss to seconds.
|
# Try to format back hh:ss to seconds.
|
||||||
return util.raw_seconds_short(string)
|
return raw_seconds_short(string)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Fall back to a plain float.
|
# Fall back to a plain float.
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -435,48 +435,6 @@ def input_select_objects(prompt, objs, rep, prompt_all=None):
|
||||||
return []
|
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.
|
# Colorization.
|
||||||
|
|
||||||
# ANSI terminal colorization code heavily inspired by pygments:
|
# ANSI terminal colorization code heavily inspired by pygments:
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ from beets.util import (
|
||||||
normpath,
|
normpath,
|
||||||
syspath,
|
syspath,
|
||||||
)
|
)
|
||||||
|
from beets.util.units import human_bytes, human_seconds, human_seconds_short
|
||||||
|
|
||||||
from . import _store_dict
|
from . import _store_dict
|
||||||
|
|
||||||
|
|
@ -541,8 +542,8 @@ class ChangeRepresentation:
|
||||||
cur_length0 = item.length if item.length else 0
|
cur_length0 = item.length if item.length else 0
|
||||||
new_length0 = track_info.length if track_info.length else 0
|
new_length0 = track_info.length if track_info.length else 0
|
||||||
# format into string
|
# format into string
|
||||||
cur_length = f"({util.human_seconds_short(cur_length0)})"
|
cur_length = f"({human_seconds_short(cur_length0)})"
|
||||||
new_length = f"({util.human_seconds_short(new_length0)})"
|
new_length = f"({human_seconds_short(new_length0)})"
|
||||||
# colorize
|
# colorize
|
||||||
lhs_length = ui.colorize(highlight_color, cur_length)
|
lhs_length = ui.colorize(highlight_color, cur_length)
|
||||||
rhs_length = ui.colorize(highlight_color, new_length)
|
rhs_length = ui.colorize(highlight_color, new_length)
|
||||||
|
|
@ -706,14 +707,14 @@ class AlbumChange(ChangeRepresentation):
|
||||||
for track_info in self.match.extra_tracks:
|
for track_info in self.match.extra_tracks:
|
||||||
line = f" ! {track_info.title} (#{self.format_index(track_info)})"
|
line = f" ! {track_info.title} (#{self.format_index(track_info)})"
|
||||||
if track_info.length:
|
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))
|
print_(ui.colorize("text_warning", line))
|
||||||
if self.match.extra_items:
|
if self.match.extra_items:
|
||||||
print_(f"Unmatched tracks ({len(self.match.extra_items)}):")
|
print_(f"Unmatched tracks ({len(self.match.extra_items)}):")
|
||||||
for item in self.match.extra_items:
|
for item in self.match.extra_items:
|
||||||
line = " ! {} (#{})".format(item.title, self.format_index(item))
|
line = " ! {} (#{})".format(item.title, self.format_index(item))
|
||||||
if item.length:
|
if item.length:
|
||||||
line += " ({})".format(util.human_seconds_short(item.length))
|
line += " ({})".format(human_seconds_short(item.length))
|
||||||
print_(ui.colorize("text_warning", line))
|
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
|
round(int(items[0].samplerate) / 1000, 1), items[0].bitdepth
|
||||||
)
|
)
|
||||||
summary_parts.append(sample_bits)
|
summary_parts.append(sample_bits)
|
||||||
summary_parts.append(util.human_seconds_short(total_duration))
|
summary_parts.append(human_seconds_short(total_duration))
|
||||||
summary_parts.append(ui.human_bytes(total_filesize))
|
summary_parts.append(human_bytes(total_filesize))
|
||||||
|
|
||||||
return ", ".join(summary_parts)
|
return ", ".join(summary_parts)
|
||||||
|
|
||||||
|
|
@ -1906,7 +1907,7 @@ def show_stats(lib, query, exact):
|
||||||
if item.album_id:
|
if item.album_id:
|
||||||
albums.add(item.album_id)
|
albums.add(item.album_id)
|
||||||
|
|
||||||
size_str = "" + ui.human_bytes(total_size)
|
size_str = "" + human_bytes(total_size)
|
||||||
if exact:
|
if exact:
|
||||||
size_str += f" ({total_size} bytes)"
|
size_str += f" ({total_size} bytes)"
|
||||||
|
|
||||||
|
|
@ -1918,7 +1919,7 @@ Artists: {}
|
||||||
Albums: {}
|
Albums: {}
|
||||||
Album artists: {}""".format(
|
Album artists: {}""".format(
|
||||||
total_items,
|
total_items,
|
||||||
ui.human_seconds(total_time),
|
human_seconds(total_time),
|
||||||
f" ({total_time:.2f} seconds)" if exact else "",
|
f" ({total_time:.2f} seconds)" if exact else "",
|
||||||
"Total size" if exact else "Approximate total size",
|
"Total size" if exact else "Approximate total size",
|
||||||
size_str,
|
size_str,
|
||||||
|
|
|
||||||
|
|
@ -1019,27 +1019,6 @@ def case_sensitive(path: bytes) -> bool:
|
||||||
return not os.path.samefile(lower_sys, upper_sys)
|
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:
|
def asciify_path(path: str, sep_replace: str) -> str:
|
||||||
"""Decodes all unicode characters in a path into ASCII equivalents.
|
"""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 import config, ui
|
||||||
from beets.test import _common
|
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):
|
class InputMethodsTest(BeetsTestCase):
|
||||||
|
|
@ -88,42 +88,6 @@ class InputMethodsTest(BeetsTestCase):
|
||||||
assert items == ["1", "3"]
|
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):
|
class ParentalDirCreation(BeetsTestCase):
|
||||||
def test_create_yes(self):
|
def test_create_yes(self):
|
||||||
non_exist_path = _common.os.fsdecode(
|
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