absfuyu 5.6.1__py3-none-any.whl → 6.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of absfuyu might be problematic. Click here for more details.
- absfuyu/__init__.py +5 -3
- absfuyu/__main__.py +2 -2
- absfuyu/cli/__init__.py +13 -2
- absfuyu/cli/audio_group.py +98 -0
- absfuyu/cli/color.py +2 -2
- absfuyu/cli/config_group.py +2 -2
- absfuyu/cli/do_group.py +2 -2
- absfuyu/cli/game_group.py +20 -2
- absfuyu/cli/tool_group.py +68 -4
- absfuyu/config/__init__.py +3 -3
- absfuyu/core/__init__.py +10 -6
- absfuyu/core/baseclass.py +104 -34
- absfuyu/core/baseclass2.py +43 -2
- absfuyu/core/decorator.py +2 -2
- absfuyu/core/docstring.py +4 -2
- absfuyu/core/dummy_cli.py +3 -3
- absfuyu/core/dummy_func.py +2 -2
- absfuyu/dxt/__init__.py +2 -2
- absfuyu/dxt/base_type.py +93 -0
- absfuyu/dxt/dictext.py +188 -6
- absfuyu/dxt/dxt_support.py +2 -2
- absfuyu/dxt/intext.py +72 -4
- absfuyu/dxt/listext.py +495 -23
- absfuyu/dxt/strext.py +2 -2
- absfuyu/extra/__init__.py +2 -2
- absfuyu/extra/audio/__init__.py +8 -0
- absfuyu/extra/audio/_util.py +57 -0
- absfuyu/extra/audio/convert.py +192 -0
- absfuyu/extra/audio/lossless.py +281 -0
- absfuyu/extra/beautiful.py +2 -2
- absfuyu/extra/da/__init__.py +39 -3
- absfuyu/extra/da/dadf.py +458 -29
- absfuyu/extra/da/dadf_base.py +2 -2
- absfuyu/extra/da/df_func.py +89 -5
- absfuyu/extra/da/mplt.py +2 -2
- absfuyu/extra/ggapi/__init__.py +8 -0
- absfuyu/extra/ggapi/gdrive.py +223 -0
- absfuyu/extra/ggapi/glicense.py +148 -0
- absfuyu/extra/ggapi/glicense_df.py +186 -0
- absfuyu/extra/ggapi/gsheet.py +88 -0
- absfuyu/extra/img/__init__.py +30 -0
- absfuyu/extra/img/converter.py +402 -0
- absfuyu/extra/img/dup_check.py +291 -0
- absfuyu/extra/pdf.py +4 -6
- absfuyu/extra/rclone.py +253 -0
- absfuyu/extra/xml.py +90 -0
- absfuyu/fun/__init__.py +2 -20
- absfuyu/fun/rubik.py +2 -2
- absfuyu/fun/tarot.py +2 -2
- absfuyu/game/__init__.py +2 -2
- absfuyu/game/game_stat.py +2 -2
- absfuyu/game/schulte.py +78 -0
- absfuyu/game/sudoku.py +2 -2
- absfuyu/game/tictactoe.py +2 -2
- absfuyu/game/wordle.py +6 -4
- absfuyu/general/__init__.py +2 -2
- absfuyu/general/content.py +2 -2
- absfuyu/general/human.py +2 -2
- absfuyu/general/resrel.py +213 -0
- absfuyu/general/shape.py +3 -8
- absfuyu/general/tax.py +344 -0
- absfuyu/logger.py +806 -59
- absfuyu/numbers/__init__.py +13 -0
- absfuyu/numbers/number_to_word.py +321 -0
- absfuyu/numbers/shorten_number.py +303 -0
- absfuyu/numbers/time_duration.py +217 -0
- absfuyu/pkg_data/__init__.py +2 -2
- absfuyu/pkg_data/deprecated.py +2 -2
- absfuyu/pkg_data/logo.py +1462 -0
- absfuyu/sort.py +4 -4
- absfuyu/tools/__init__.py +2 -2
- absfuyu/tools/checksum.py +119 -4
- absfuyu/tools/converter.py +2 -2
- absfuyu/tools/generator.py +24 -7
- absfuyu/tools/inspector.py +2 -2
- absfuyu/tools/keygen.py +2 -2
- absfuyu/tools/obfuscator.py +2 -2
- absfuyu/tools/passwordlib.py +2 -2
- absfuyu/tools/shutdownizer.py +3 -8
- absfuyu/tools/sw.py +213 -10
- absfuyu/tools/web.py +10 -13
- absfuyu/typings.py +5 -8
- absfuyu/util/__init__.py +31 -2
- absfuyu/util/api.py +7 -4
- absfuyu/util/cli.py +119 -0
- absfuyu/util/gui.py +91 -0
- absfuyu/util/json_method.py +2 -2
- absfuyu/util/lunar.py +2 -2
- absfuyu/util/package.py +124 -0
- absfuyu/util/path.py +313 -4
- absfuyu/util/performance.py +2 -2
- absfuyu/util/shorten_number.py +206 -13
- absfuyu/util/text_table.py +2 -2
- absfuyu/util/zipped.py +2 -2
- absfuyu/version.py +22 -19
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/METADATA +37 -8
- absfuyu-6.1.3.dist-info/RECORD +105 -0
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/WHEEL +1 -1
- absfuyu/extra/data_analysis.py +0 -21
- absfuyu-5.6.1.dist-info/RECORD +0 -79
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/entry_points.txt +0 -0
- {absfuyu-5.6.1.dist-info → absfuyu-6.1.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Absfuyu: Audio
|
|
3
|
+
--------------
|
|
4
|
+
Audio convert
|
|
5
|
+
|
|
6
|
+
Version: 6.1.2
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Module level
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
__all__ = ["DirectoryAudioConvertMixin"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Library
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
import json
|
|
18
|
+
import subprocess
|
|
19
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Literal
|
|
22
|
+
|
|
23
|
+
from absfuyu.core.dummy_func import tqdm
|
|
24
|
+
from absfuyu.extra.audio._util import ResultStatus as ConvertStatus
|
|
25
|
+
from absfuyu.extra.audio._util import StatusCode
|
|
26
|
+
from absfuyu.util import is_command_available
|
|
27
|
+
from absfuyu.util.path import DirectoryBase
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Class
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
class DirectoryAudioConvertMixin(DirectoryBase):
|
|
33
|
+
"""
|
|
34
|
+
Directory - Audio convert to mp3
|
|
35
|
+
|
|
36
|
+
- convert_to_mp3
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, source_path, create_if_not_exist=False):
|
|
40
|
+
super().__init__(source_path, create_if_not_exist)
|
|
41
|
+
is_command_available(["ffmpeg"], "ERROR: ffmpeg not installed to PATH")
|
|
42
|
+
|
|
43
|
+
def _read_metadata(self, audio_path: Path, /) -> dict:
|
|
44
|
+
"""
|
|
45
|
+
Read audio metadata
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
audio_path : Path
|
|
50
|
+
Path to audio
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
dict
|
|
55
|
+
Audio's metadata
|
|
56
|
+
"""
|
|
57
|
+
cmd = [
|
|
58
|
+
"ffprobe",
|
|
59
|
+
"-v",
|
|
60
|
+
"quiet",
|
|
61
|
+
"-print_format",
|
|
62
|
+
"json",
|
|
63
|
+
"-show_format",
|
|
64
|
+
str(audio_path.resolve()),
|
|
65
|
+
]
|
|
66
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
67
|
+
data = json.loads(result.stdout)
|
|
68
|
+
|
|
69
|
+
if "format" in data and "tags" in data["format"]:
|
|
70
|
+
return data["format"]["tags"]
|
|
71
|
+
return {}
|
|
72
|
+
|
|
73
|
+
def convert_one(self, audio_path: Path | str, bitrate: Literal["128k", "320k"] = "320k") -> ConvertStatus:
|
|
74
|
+
"""
|
|
75
|
+
Convert audio to mp3
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
audio_path : Path | str
|
|
80
|
+
Path to audio
|
|
81
|
+
|
|
82
|
+
bitrate : Literal["128k", "320k"], optional
|
|
83
|
+
Bitrate, by default "320k"
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
ConvertStatus
|
|
88
|
+
Result
|
|
89
|
+
"""
|
|
90
|
+
audio_path = Path(audio_path)
|
|
91
|
+
mp3_path = audio_path.with_suffix(".mp3")
|
|
92
|
+
|
|
93
|
+
if mp3_path.exists():
|
|
94
|
+
return ConvertStatus(StatusCode.SKIP, mp3_path)
|
|
95
|
+
|
|
96
|
+
metadata = self._read_metadata(audio_path)
|
|
97
|
+
|
|
98
|
+
ffmpeg_cmd = ["ffmpeg", "-y", "-i", str(audio_path.resolve())]
|
|
99
|
+
|
|
100
|
+
# Add metadata tags
|
|
101
|
+
for key, value in metadata.items():
|
|
102
|
+
ffmpeg_cmd.extend(["-metadata", f"{key}={value}"])
|
|
103
|
+
|
|
104
|
+
ffmpeg_cmd.extend(["-vn", "-codec:a", "libmp3lame", "-b:a", bitrate, str(mp3_path.resolve())])
|
|
105
|
+
|
|
106
|
+
subprocess.run(ffmpeg_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
107
|
+
return ConvertStatus(StatusCode.OK, mp3_path)
|
|
108
|
+
|
|
109
|
+
def convert_to_mp3_single_thread(
|
|
110
|
+
self,
|
|
111
|
+
from_format: str = ".flac",
|
|
112
|
+
recursive: bool = True,
|
|
113
|
+
bitrate: Literal["128k", "320k"] = "320k",
|
|
114
|
+
) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Convert audios to .mp3 - Single thread
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
from_format : str, optional
|
|
121
|
+
Audio format, by default ".flac"
|
|
122
|
+
|
|
123
|
+
recursive : bool, optional
|
|
124
|
+
Include audio in child folder, by default True
|
|
125
|
+
|
|
126
|
+
bitrate : Literal["128k", "320k"], optional
|
|
127
|
+
Bitrate, by default "320k"
|
|
128
|
+
"""
|
|
129
|
+
audios = list(self.source_path.rglob(f"{'**/*'if recursive else '*'}{from_format}"))
|
|
130
|
+
|
|
131
|
+
if not audios:
|
|
132
|
+
print(f"No {from_format.upper()} files found.")
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
print(f"Found {len(audios)} {from_format.upper()} files.")
|
|
136
|
+
|
|
137
|
+
results: list[ConvertStatus] = []
|
|
138
|
+
for x in tqdm(audios, desc="Converting", unit="file", unit_scale=True):
|
|
139
|
+
results.append(self.convert_one(x, bitrate=bitrate))
|
|
140
|
+
|
|
141
|
+
print("\n--- Summary ---")
|
|
142
|
+
for line in results:
|
|
143
|
+
line.print()
|
|
144
|
+
|
|
145
|
+
def convert_to_mp3(
|
|
146
|
+
self,
|
|
147
|
+
from_format: str = ".flac",
|
|
148
|
+
workers: int | None = None,
|
|
149
|
+
recursive: bool = True,
|
|
150
|
+
bitrate: Literal["128k", "320k"] = "320k",
|
|
151
|
+
) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Convert audios to .mp3
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
from_format : str, optional
|
|
158
|
+
Audio format, by default ".flac"
|
|
159
|
+
|
|
160
|
+
workers : int | None, optional
|
|
161
|
+
Number of parallel processing threads, by default None
|
|
162
|
+
|
|
163
|
+
recursive : bool, optional
|
|
164
|
+
Include audio in child folder, by default True
|
|
165
|
+
|
|
166
|
+
bitrate : Literal["128k", "320k"], optional
|
|
167
|
+
Bitrate, by default "320k"
|
|
168
|
+
"""
|
|
169
|
+
audios = list(self.source_path.rglob(f"{'**/*'if recursive else '*'}{from_format}"))
|
|
170
|
+
|
|
171
|
+
if not audios:
|
|
172
|
+
print(f"No {from_format.upper()} files found.")
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
print(f"Found {len(audios)} {from_format.upper()} files.")
|
|
176
|
+
|
|
177
|
+
results: list[ConvertStatus] = []
|
|
178
|
+
with ProcessPoolExecutor(max_workers=workers) as executor:
|
|
179
|
+
futures = {executor.submit(self.convert_one, x, bitrate): x for x in audios}
|
|
180
|
+
|
|
181
|
+
for fut in tqdm(
|
|
182
|
+
as_completed(futures),
|
|
183
|
+
total=len(futures),
|
|
184
|
+
desc="Converting",
|
|
185
|
+
unit="file",
|
|
186
|
+
unit_scale=True,
|
|
187
|
+
):
|
|
188
|
+
results.append(fut.result())
|
|
189
|
+
|
|
190
|
+
print("\n--- Summary ---")
|
|
191
|
+
for line in results:
|
|
192
|
+
line.print()
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Absfuyu: Audio
|
|
3
|
+
--------------
|
|
4
|
+
Audio lossless checker
|
|
5
|
+
|
|
6
|
+
Version: 6.1.2
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# Module level
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
__all__ = ["AudioInfo", "DirectoryAudioLosslessCheckMixin"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Library
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
import json
|
|
18
|
+
import subprocess
|
|
19
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, NamedTuple
|
|
23
|
+
|
|
24
|
+
from absfuyu.core.dummy_func import tqdm
|
|
25
|
+
from absfuyu.extra.audio._util import ResultStatus, StatusCode
|
|
26
|
+
from absfuyu.util import is_command_available
|
|
27
|
+
from absfuyu.util.path import DirectoryBase
|
|
28
|
+
from absfuyu.util.shorten_number import Duration
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
import numpy as np
|
|
32
|
+
except ImportError:
|
|
33
|
+
from subprocess import run
|
|
34
|
+
|
|
35
|
+
from absfuyu.config import ABSFUYU_CONFIG
|
|
36
|
+
|
|
37
|
+
if ABSFUYU_CONFIG._get_setting("auto-install-extra").value: # type: ignore
|
|
38
|
+
cmd = "python -m pip install -U absfuyu[extra]".split()
|
|
39
|
+
run(cmd)
|
|
40
|
+
else:
|
|
41
|
+
raise SystemExit("This feature is in absfuyu[extra] package") # noqa: B904
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Class
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
class FrequencyRange(NamedTuple):
|
|
47
|
+
"""Audio frequency range"""
|
|
48
|
+
|
|
49
|
+
min: int | float
|
|
50
|
+
max: int | float
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class AudioInfo:
|
|
55
|
+
"""
|
|
56
|
+
Audio infomation
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
sample_rate : int | None, optional
|
|
61
|
+
Sample rate (Hz)
|
|
62
|
+
|
|
63
|
+
bit_depth : int, optional
|
|
64
|
+
Bit depth
|
|
65
|
+
|
|
66
|
+
channels : int | None, optional
|
|
67
|
+
Number of channels
|
|
68
|
+
|
|
69
|
+
codec : str | None, optional
|
|
70
|
+
Audio codec
|
|
71
|
+
|
|
72
|
+
duration_raw : float, optional
|
|
73
|
+
Duration of audio (second)
|
|
74
|
+
|
|
75
|
+
bitrate : Any | None, optional
|
|
76
|
+
Bitrate
|
|
77
|
+
|
|
78
|
+
audio_path : Path | None, optional
|
|
79
|
+
Path to audio
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
AudioInfo
|
|
84
|
+
Audio infomation
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
sample_rate: int | None = field(default=None, metadata={"unit": "Hz"})
|
|
88
|
+
bit_depth: int = field(default=0, metadata={"unit": "bit"})
|
|
89
|
+
channels: int | None = field(default=None)
|
|
90
|
+
codec: str | None = field(default=None)
|
|
91
|
+
duration_raw: float = field(default=0.0, repr=False, metadata={"unit": "second"})
|
|
92
|
+
bitrate: int | None = field(default=None)
|
|
93
|
+
audio_path: Path | None = field(default=None, repr=False) # hide path in repr
|
|
94
|
+
|
|
95
|
+
# computed fields
|
|
96
|
+
duration: float = field(init=False)
|
|
97
|
+
freq_range: FrequencyRange = field(init=False, metadata={"unit": "Hz"})
|
|
98
|
+
|
|
99
|
+
def __post_init__(self):
|
|
100
|
+
self.duration = Duration(self.duration_raw)
|
|
101
|
+
|
|
102
|
+
# Resource intensive method
|
|
103
|
+
# _, sampling_rate = librosa.load(audio_path, sr=None)
|
|
104
|
+
# freqs = librosa.fft_frequencies(sr=sampling_rate)
|
|
105
|
+
freqs = np.fft.rfftfreq(n=2048, d=1.0 / self.sample_rate)
|
|
106
|
+
self.freq_range = FrequencyRange(freqs[0], freqs[-1])
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def is_lossless(self) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Audio is lossless when frequencies above 20,000 are not cut off
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
bool
|
|
116
|
+
If audio is lossless
|
|
117
|
+
"""
|
|
118
|
+
return self.freq_range[1] >= 20000
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def is_hi_res(self) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
Audio is HiRes when lossless and have sample rate >= 48,000Hz or bit rate >= 16 bit
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
bool
|
|
128
|
+
If audio is lossless
|
|
129
|
+
"""
|
|
130
|
+
bd = 0 if self.bit_depth is None else self.bit_depth
|
|
131
|
+
return all([self.sample_rate >= 48000, bd >= 24, self.is_lossless])
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class DirectoryAudioLosslessCheckMixin(DirectoryBase):
|
|
135
|
+
"""
|
|
136
|
+
Directory - Audio lossless checker
|
|
137
|
+
|
|
138
|
+
- lossless_check
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def get_audio_info(self, audio_path: Path, /) -> AudioInfo:
|
|
142
|
+
"""
|
|
143
|
+
Return audio info using ffprobe.
|
|
144
|
+
|
|
145
|
+
Parameters
|
|
146
|
+
----------
|
|
147
|
+
audio_path : Path
|
|
148
|
+
Path to audio
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
AudioInfo
|
|
153
|
+
Audio infomation
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
is_command_available(["ffmpeg"], "ERROR: ffmpeg not installed to PATH")
|
|
157
|
+
|
|
158
|
+
# Probe
|
|
159
|
+
cmd = [
|
|
160
|
+
"ffprobe",
|
|
161
|
+
"-v",
|
|
162
|
+
"error",
|
|
163
|
+
"-print_format",
|
|
164
|
+
"json",
|
|
165
|
+
"-show_streams",
|
|
166
|
+
"-select_streams",
|
|
167
|
+
"a:0",
|
|
168
|
+
str(audio_path.resolve()),
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
172
|
+
data: dict[str, Any] = json.loads(result.stdout)["streams"][0]
|
|
173
|
+
|
|
174
|
+
bit_depth = data.get("bits_per_sample") or data.get("bits_per_raw_sample")
|
|
175
|
+
if bit_depth is not None:
|
|
176
|
+
bit_depth = int(bit_depth)
|
|
177
|
+
|
|
178
|
+
ai = AudioInfo(
|
|
179
|
+
sample_rate=int(data.get("sample_rate", 0)),
|
|
180
|
+
bit_depth=bit_depth,
|
|
181
|
+
channels=data.get("channels"),
|
|
182
|
+
codec=data.get("codec_name"),
|
|
183
|
+
duration_raw=float(data["duration"]) if "duration" in data else None,
|
|
184
|
+
bitrate=int(data["bit_rate"]) if "bit_rate" in data else None,
|
|
185
|
+
audio_path=audio_path,
|
|
186
|
+
)
|
|
187
|
+
return ai
|
|
188
|
+
|
|
189
|
+
def lossless_check_one(self, audio_path: Path) -> ResultStatus:
|
|
190
|
+
"""
|
|
191
|
+
Check if audio is lossless
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
audio_path : Path
|
|
196
|
+
Path to audio
|
|
197
|
+
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
ResultStatus
|
|
201
|
+
If audio is lossless
|
|
202
|
+
"""
|
|
203
|
+
res = self.get_audio_info(audio_path)
|
|
204
|
+
if res.is_hi_res:
|
|
205
|
+
return ResultStatus(StatusCode.HIRES, audio_path)
|
|
206
|
+
elif res.is_lossless:
|
|
207
|
+
return ResultStatus(StatusCode.LOSSLESS, audio_path)
|
|
208
|
+
return ResultStatus(StatusCode.NOT_LOSSLESS, audio_path)
|
|
209
|
+
|
|
210
|
+
def lossless_check_single_thread(self, from_format: str = ".flac", recursive: bool = True) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Check if audios in directory are lossless - single thread version
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
from_format : str, optional
|
|
217
|
+
Audio format, by default ".flac"
|
|
218
|
+
|
|
219
|
+
recursive : bool, optional
|
|
220
|
+
Include audio in child folder, by default True
|
|
221
|
+
"""
|
|
222
|
+
audios = list(self.source_path.rglob(f"{'**/*'if recursive else '*'}{from_format}"))
|
|
223
|
+
|
|
224
|
+
if not audios:
|
|
225
|
+
print(f"No {from_format.upper()} files found.")
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
print(f"Found {len(audios)} {from_format.upper()} files.")
|
|
229
|
+
|
|
230
|
+
results: list[ResultStatus] = []
|
|
231
|
+
for x in tqdm(audios, desc="Checking", unit="file", unit_scale=True):
|
|
232
|
+
results.append(self.lossless_check_one(x))
|
|
233
|
+
|
|
234
|
+
print("\n--- Summary ---")
|
|
235
|
+
for line in results:
|
|
236
|
+
line.print()
|
|
237
|
+
|
|
238
|
+
def lossless_check(
|
|
239
|
+
self,
|
|
240
|
+
from_format: str = ".flac",
|
|
241
|
+
recursive: bool = True,
|
|
242
|
+
workers: int | None = None,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Check if audios in directory are lossless
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
from_format : str, optional
|
|
250
|
+
Audio format, by default ".flac"
|
|
251
|
+
|
|
252
|
+
recursive : bool, optional
|
|
253
|
+
Include audio in child folder, by default True
|
|
254
|
+
|
|
255
|
+
workers : int | None, optional
|
|
256
|
+
Number of parallel processing threads, by default None
|
|
257
|
+
"""
|
|
258
|
+
audios = list(self.source_path.rglob(f"{'**/*'if recursive else '*'}{from_format}"))
|
|
259
|
+
|
|
260
|
+
if not audios:
|
|
261
|
+
print(f"No {from_format.upper()} files found.")
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
print(f"Found {len(audios)} {from_format.upper()} files.")
|
|
265
|
+
|
|
266
|
+
results: list[ResultStatus] = []
|
|
267
|
+
with ProcessPoolExecutor(max_workers=workers) as executor:
|
|
268
|
+
futures = {executor.submit(self.lossless_check_one, x): x for x in audios}
|
|
269
|
+
|
|
270
|
+
for fut in tqdm(
|
|
271
|
+
as_completed(futures),
|
|
272
|
+
total=len(futures),
|
|
273
|
+
desc="Checking",
|
|
274
|
+
unit="file",
|
|
275
|
+
unit_scale=True,
|
|
276
|
+
):
|
|
277
|
+
results.append(fut.result())
|
|
278
|
+
|
|
279
|
+
print("\n--- Summary ---")
|
|
280
|
+
for line in results:
|
|
281
|
+
line.print()
|
absfuyu/extra/beautiful.py
CHANGED
absfuyu/extra/da/__init__.py
CHANGED
|
@@ -3,13 +3,19 @@ Absfuyu: Data Analysis
|
|
|
3
3
|
----------------------
|
|
4
4
|
Data Analyst
|
|
5
5
|
|
|
6
|
-
Version:
|
|
7
|
-
Date updated: 12/
|
|
6
|
+
Version: 6.1.2
|
|
7
|
+
Date updated: 30/12/2025 (dd/mm/yyyy)
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
# Module level
|
|
11
11
|
# ---------------------------------------------------------------------------
|
|
12
|
-
__all__ = [
|
|
12
|
+
__all__ = [
|
|
13
|
+
"MatplotlibFormatString",
|
|
14
|
+
"DADF",
|
|
15
|
+
# Function
|
|
16
|
+
"custom_pandas_settings",
|
|
17
|
+
"reset_custom_pandas_settings",
|
|
18
|
+
]
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
# Library
|
|
@@ -18,7 +24,9 @@ DA_MODE = False
|
|
|
18
24
|
|
|
19
25
|
try:
|
|
20
26
|
import numpy as np
|
|
27
|
+
import openpyxl
|
|
21
28
|
import pandas as pd
|
|
29
|
+
import xlsxwriter
|
|
22
30
|
except ImportError:
|
|
23
31
|
from subprocess import run
|
|
24
32
|
|
|
@@ -34,3 +42,31 @@ else:
|
|
|
34
42
|
|
|
35
43
|
from absfuyu.extra.da.dadf import DADF
|
|
36
44
|
from absfuyu.extra.da.mplt import MatplotlibFormatString
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Function
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
def custom_pandas_settings(*, show_all_rows: bool = False) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Custom pandas settings. Currently only show all cols/rows
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
show_all_rows : bool, optional
|
|
56
|
+
Show all rows, by default False
|
|
57
|
+
"""
|
|
58
|
+
# Shows all columns
|
|
59
|
+
pd.set_option("display.max_columns", None) # type: ignore
|
|
60
|
+
|
|
61
|
+
if show_all_rows:
|
|
62
|
+
# (optional) also show all rows if needed
|
|
63
|
+
pd.set_option("display.max_rows", None) # type: ignore
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def reset_custom_pandas_settings() -> None:
|
|
67
|
+
"""
|
|
68
|
+
Reset custom pandas settings
|
|
69
|
+
"""
|
|
70
|
+
settings = ["display.max_columns", "display.max_rows"]
|
|
71
|
+
for x in settings:
|
|
72
|
+
pd.reset_option(x) # type: ignore
|