1
0
mirror of synced 2024-12-04 19:17:55 +01:00

Standardize loading and dumping routines for formats that use one file per chart

This commit is contained in:
Stepland 2021-05-09 16:31:53 +02:00
parent bfd0dffa85
commit 25b6529245
6 changed files with 116 additions and 122 deletions

18
.flake8
View File

@ -1,19 +1,19 @@
[flake8] [flake8]
ignore = ignore =
# break after binary op. # First category : "ffs flake8 let black do its own formatting"
# black does its thing and I leave it that way
W503
# f-string is missing placeholders
# really don't want to be bothered for so little
F541
# whitespace before ':' # whitespace before ':'
# let black handle that
E203 E203
# blank line contains whitespace # blank line contains whitespace
# let black handle that as well
W293 W293
# expected 2 blank lines, found 1
E302
# break after binary op.
W503
# Second category : "I know what I'm doing leave me alone"
# f-string is missing placeholders
F541
# do not assign a lambda expression, use a def # do not assign a lambda expression, use a def
# I know when I need that one, thank you very much
E731 E731
exclude = exclude =
.git .git

View File

@ -1,78 +0,0 @@
"""Things that make it easier to integrate formats with different opinions
on song folder structure"""
from itertools import count
from typing import TypedDict, Iterator, Any, Dict, AbstractSet
from pathlib import Path
from jubeatools.song import Song
from jubeatools.formats.typing import ChartFileDumper, Dumper
from jubeatools.formats.filetypes import ChartFile
from jubeatools.formats.dump_tools import DIFFICULTY_INDEX, DIFFICULTY_NUMBER
def make_dumper_from_chart_file_dumper(
internal_dumper: ChartFileDumper,
file_name_template: Path,
) -> Dumper:
"""Adapt a ChartFileDumper to the Dumper protocol, The resulting function
uses the file name template if it recieves an existing directory as an
output path"""
def dumper(song: Song, path: Path, **kwargs: Any) -> Dict[Path, bytes]:
res: Dict[Path, bytes] = {}
if path.is_dir():
file_path = file_name_template
else:
file_path = path
name_format = f"{file_path.stem}{{dedup}}{file_path.suffix}"
files = internal_dumper(song, **kwargs)
for chartfile in files:
filepath = choose_file_path(chartfile, name_format, path.parent, res.keys())
res[filepath] = chartfile.contents
return res
return dumper
def choose_file_path(
chart_file: ChartFile,
name_format: str,
parent: Path,
already_chosen: AbstractSet[Path],
) -> Path:
all_paths = iter_possible_paths(chart_file, name_format, parent)
not_on_filesystem = filter(lambda p: not p.exists(), all_paths)
not_already_chosen = filter(lambda p: p not in already_chosen, not_on_filesystem)
return next(not_already_chosen)
def iter_possible_paths(
chart_file: ChartFile, name_format: str, parent: Path
) -> Iterator[Path]:
for dedup_index in count(start=0):
params = extract_format_params(chart_file, dedup_index)
filename = name_format.format(**params).strip()
yield parent / filename
class FormatParameters(TypedDict, total=False):
title: str
difficulty: str
# 0-based
difficulty_index: int
# 1-based
difficulty_number: int
dedup: str
def extract_format_params(chartfile: ChartFile, dedup_index: int) -> FormatParameters:
return FormatParameters(
title=chartfile.song.metadata.title or "",
difficulty=chartfile.difficulty,
difficulty_index=DIFFICULTY_INDEX.get(chartfile.difficulty, 3),
difficulty_number=DIFFICULTY_NUMBER.get(chartfile.difficulty, 4),
dedup="" if dedup_index else f"-{dedup_index}",
)

View File

@ -1,6 +1,10 @@
from jubeatools.song import Difficulty from itertools import count
from typing import Dict from pathlib import Path
from typing import AbstractSet, Any, Dict, Iterator, TypedDict
from jubeatools.formats.filetypes import ChartFile
from jubeatools.formats.typing import ChartFileDumper, Dumper
from jubeatools.song import Difficulty, Song
DIFFICULTY_NUMBER: Dict[str, int] = { DIFFICULTY_NUMBER: Dict[str, int] = {
Difficulty.BASIC: 1, Difficulty.BASIC: 1,
@ -13,3 +17,70 @@ DIFFICULTY_INDEX: Dict[str, int] = {
Difficulty.ADVANCED: 1, Difficulty.ADVANCED: 1,
Difficulty.EXTREME: 2, Difficulty.EXTREME: 2,
} }
def make_dumper_from_chart_file_dumper(
internal_dumper: ChartFileDumper,
file_name_template: Path,
) -> Dumper:
"""Adapt a ChartFileDumper to the Dumper protocol, The resulting function
uses the file name template if it recieves an existing directory as an
output path"""
def dumper(song: Song, path: Path, **kwargs: Any) -> Dict[Path, bytes]:
res: Dict[Path, bytes] = {}
if path.is_dir():
file_path = file_name_template
else:
file_path = path
name_format = f"{file_path.stem}{{dedup}}{file_path.suffix}"
files = internal_dumper(song, **kwargs)
for chartfile in files:
filepath = choose_file_path(chartfile, name_format, path.parent, res.keys())
res[filepath] = chartfile.contents
return res
return dumper
def choose_file_path(
chart_file: ChartFile,
name_format: str,
parent: Path,
already_chosen: AbstractSet[Path],
) -> Path:
all_paths = iter_possible_paths(chart_file, name_format, parent)
not_on_filesystem = filter(lambda p: not p.exists(), all_paths)
not_already_chosen = filter(lambda p: p not in already_chosen, not_on_filesystem)
return next(not_already_chosen)
def iter_possible_paths(
chart_file: ChartFile, name_format: str, parent: Path
) -> Iterator[Path]:
for dedup_index in count(start=0):
params = extract_format_params(chart_file, dedup_index)
filename = name_format.format(**params).strip()
yield parent / filename
class FormatParameters(TypedDict, total=False):
title: str
difficulty: str
# 0-based
difficulty_index: int
# 1-based
difficulty_number: int
dedup: str
def extract_format_params(chartfile: ChartFile, dedup_index: int) -> FormatParameters:
return FormatParameters(
title=chartfile.song.metadata.title or "",
difficulty=chartfile.difficulty,
difficulty_index=DIFFICULTY_INDEX.get(chartfile.difficulty, 3),
difficulty_number=DIFFICULTY_NUMBER.get(chartfile.difficulty, 4),
dedup="" if dedup_index else f"-{dedup_index}",
)

View File

@ -1,20 +0,0 @@
from pathlib import Path
from typing import Dict, List, Optional
from jubeatools.formats.files import make_folder_loader
def read_jubeat_analyser_file(path: Path) -> Optional[List[str]]:
try:
# The vast majority of memo files you will encounter will be propely
# decoded using shift-jis-2004. Get ready for endless fun with the small
# portion of files that won't
lines = path.read_text(encoding="shift-jis-2004").split("\n")
except UnicodeDecodeError:
return None
else:
return lines
load_folder = make_folder_loader(
glob_pattern="*.txt",
file_loader=read_jubeat_analyser_file,
)

View File

@ -7,13 +7,15 @@ from copy import deepcopy
from dataclasses import astuple, dataclass from dataclasses import astuple, dataclass
from decimal import Decimal from decimal import Decimal
from itertools import product, zip_longest from itertools import product, zip_longest
from pathlib import Path
from typing import AbstractSet, Dict, List, Optional, Set, Tuple, Union from typing import AbstractSet, Dict, List, Optional, Set, Tuple, Union
import constraint import constraint
from parsimonious import Grammar, NodeVisitor, ParseError from parsimonious import Grammar, NodeVisitor, ParseError
from parsimonious.nodes import Node from parsimonious.nodes import Node
from jubeatools.song import BeatsTime, BPMEvent, LongNote, NotePosition, Difficulty from jubeatools.formats.load_tools import make_folder_loader
from jubeatools.song import BeatsTime, BPMEvent, Difficulty, LongNote, NotePosition
from .symbols import ( from .symbols import (
CIRCLE_FREE_SYMBOLS, CIRCLE_FREE_SYMBOLS,
@ -461,3 +463,21 @@ class DoubleColumnFrame:
line += [f"|{''.join(time)}|"] line += [f"|{''.join(time)}|"]
res += [" ".join(line)] res += [" ".join(line)]
return "\n".join(res) return "\n".join(res)
def read_jubeat_analyser_file(path: Path) -> Optional[List[str]]:
try:
# The vast majority of memo files you will encounter will be propely
# decoded using shift-jis-2004. Get ready for endless fun with the small
# portion of files that won't
lines = path.read_text(encoding="shift-jis-2004").split("\n")
except UnicodeDecodeError:
return None
else:
return lines
load_folder = make_folder_loader(
glob_pattern="*.txt",
file_loader=read_jubeat_analyser_file,
)

View File

@ -1,40 +1,41 @@
from pathlib import Path from pathlib import Path
from typing import Dict, List, TypeVar, Protocol, Generic, Optional from typing import Dict, Iterable, Optional, Protocol, TypeVar
T = TypeVar("T") T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True)
class FileLoader(Protocol, Generic[T]):
class FileLoader(Protocol[T_co]):
"""Function that excepts a path to a file as a parameter and returns its """Function that excepts a path to a file as a parameter and returns its
contents in whatever form suitable for the current format. Returns None in contents in whatever form suitable for the current format. Returns None in
case of error""" case of error"""
def __call__(self, path: Path) -> Optional[T]:
def __call__(self, path: Path) -> T_co:
... ...
class FolderLoader(Protocol, Generic[T]):
class FolderLoader(Protocol[T]):
"""Function that expects a folder or a file path as a parameter. Loads """Function that expects a folder or a file path as a parameter. Loads
either all valid files in the folder or just the given file depending on either all valid files in the folder or just the given file depending on
the argument""" the argument"""
def __call__(self, path: Path) -> Dict[Path, T]: def __call__(self, path: Path) -> Dict[Path, T]:
... ...
def make_folder_loader(
glob_pattern: str,
file_loader: FileLoader
) -> FolderLoader:
def make_folder_loader(glob_pattern: str, file_loader: FileLoader) -> FolderLoader:
def folder_loader(path: Path) -> Dict[Path, T]: def folder_loader(path: Path) -> Dict[Path, T]:
files: Dict[Path, T] = {} files: Dict[Path, T] = {}
if path.is_dir(): if path.is_dir():
paths = path.glob(glob_pattern) paths: Iterable[Path] = path.glob(glob_pattern)
else: else:
paths = [path] paths = [path]
for p in paths: for p in paths:
value = file_loader(p) value = file_loader(p)
if value is not None: if value is not None:
files[p] = value files[p] = value
return files return files
return folder_loader return folder_loader