Add support for memon 0.3.0
This commit is contained in:
parent
93ada9fd31
commit
2e4b5978bd
4
.flake8
4
.flake8
@ -32,6 +32,6 @@ per-file-ignores =
|
||||
example*.py: F405, F403
|
||||
# Silence weird false positive on inline comments ...
|
||||
jubeatools/formats/jubeat_analyser/symbols.py: E262
|
||||
# there's a field named "l" in a marshmallow schema
|
||||
jubeatools/formats/memon/memon.py: E741
|
||||
# there's a field named "l" in a marshmallow schema and I don't want te rename it
|
||||
jubeatools/formats/memon/v0.py: E741
|
||||
max-line-length = 120
|
@ -1,3 +1,7 @@
|
||||
# v1.3.0
|
||||
## Added
|
||||
- [memon] 🎉 v0.3.0 support
|
||||
|
||||
# v1.2.3
|
||||
## Fixed
|
||||
- Loaders and Dumpers would recieve options with unwanted default values when
|
||||
|
@ -16,7 +16,8 @@ jubeatools ${source} ${destination} -f ${output format} (... format specific opt
|
||||
## Which formats are supported
|
||||
| | | input | output |
|
||||
|-----------------|----------------------|:-----:|:------:|
|
||||
| memon | v0.2.0 | ✔️ | ✔️ |
|
||||
| memon | v0.3.0 | ✔️ | ✔️ |
|
||||
| | v0.2.0 | ✔️ | ✔️ |
|
||||
| | v0.1.0 | ✔️ | ✔️ |
|
||||
| | legacy | ✔️ | ✔️ |
|
||||
| jubeat analyser | #memo2 | ✔️ | ✔️ |
|
||||
|
@ -18,7 +18,8 @@ Sanity checks before anything serious happens, from the repo's root :
|
||||
Now that this is done you can move on to actually making a new version,
|
||||
while still being in the repo's root :
|
||||
1. Update `CHANGELOG.md`
|
||||
1. Commit everything you want in the new release, including the changelog
|
||||
1. Update `README.md` if you've just added support for a new format
|
||||
1. Commit everything you want in the new release
|
||||
1. Run the script <br> `$ poetry run python utils/bump_version.py {rule}`
|
||||
|
||||
`{rule}` will usually be one of `patch`, `minor` or `major`. But it can be anything `poetry version` handles.
|
||||
|
@ -1,9 +1,10 @@
|
||||
import string
|
||||
from functools import singledispatch
|
||||
from itertools import count
|
||||
from pathlib import Path
|
||||
from typing import AbstractSet, Any, Dict, Iterator, TypedDict
|
||||
from typing import AbstractSet, Any, Dict, Iterator, Optional, TypedDict
|
||||
|
||||
from jubeatools.formats.filetypes import ChartFile
|
||||
from jubeatools.formats.filetypes import ChartFile, JubeatFile, SongFile
|
||||
from jubeatools.formats.typing import ChartFileDumper, Dumper
|
||||
from jubeatools.song import Difficulty, Song
|
||||
from jubeatools.utils import none_or
|
||||
@ -31,17 +32,12 @@ def make_dumper_from_chart_file_dumper(
|
||||
|
||||
def dumper(song: Song, path: Path, **kwargs: Any) -> Dict[Path, bytes]:
|
||||
res: Dict[Path, bytes] = {}
|
||||
if path.is_dir():
|
||||
file_path = file_name_template
|
||||
parent = path
|
||||
else:
|
||||
file_path = path
|
||||
parent = path.parent
|
||||
|
||||
name_format = f"{file_path.stem}{{dedup}}{file_path.suffix}"
|
||||
name_format = FileNameFormat(file_name_template, suggestion=path)
|
||||
files = internal_dumper(song, **kwargs)
|
||||
for chartfile in files:
|
||||
filepath = choose_file_path(chartfile, name_format, parent, res.keys())
|
||||
filepath = name_format.available_filename_for(
|
||||
chartfile, already_chosen=res.keys()
|
||||
)
|
||||
res[filepath] = chartfile.contents
|
||||
|
||||
return res
|
||||
@ -49,28 +45,6 @@ def make_dumper_from_chart_file_dumper(
|
||||
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)
|
||||
formatter = BetterStringFormatter()
|
||||
filename = formatter.format(name_format, **params).strip()
|
||||
yield parent / filename
|
||||
|
||||
|
||||
class FormatParameters(TypedDict, total=False):
|
||||
title: str
|
||||
# uppercase BSC ADV EXT
|
||||
@ -82,13 +56,64 @@ class FormatParameters(TypedDict, total=False):
|
||||
dedup: str
|
||||
|
||||
|
||||
def extract_format_params(chartfile: ChartFile, dedup_index: int) -> FormatParameters:
|
||||
class FileNameFormat:
|
||||
def __init__(self, file_name_template: Path, suggestion: Path):
|
||||
if suggestion.is_dir():
|
||||
file_path = file_name_template
|
||||
self.parent = suggestion
|
||||
else:
|
||||
file_path = suggestion
|
||||
self.parent = suggestion.parent
|
||||
|
||||
self.name_format = f"{file_path.stem}{{dedup}}{file_path.suffix}"
|
||||
|
||||
def available_filename_for(
|
||||
self, file: JubeatFile, already_chosen: Optional[AbstractSet[Path]] = None
|
||||
) -> Path:
|
||||
fixed_params = extract_format_params(file)
|
||||
return next(self.iter_possible_paths(fixed_params, already_chosen))
|
||||
|
||||
def iter_possible_paths(
|
||||
self,
|
||||
fixed_params: FormatParameters,
|
||||
already_chosen: Optional[AbstractSet[Path]] = None,
|
||||
) -> Iterator[Path]:
|
||||
all_paths = self.iter_deduped_paths(fixed_params)
|
||||
not_on_filesystem = (p for p in all_paths if not p.exists())
|
||||
if already_chosen is not None:
|
||||
yield from (p for p in not_on_filesystem if p not in already_chosen)
|
||||
else:
|
||||
yield from not_on_filesystem
|
||||
|
||||
def iter_deduped_paths(self, params: FormatParameters) -> Iterator[Path]:
|
||||
for dedup_index in count(start=0):
|
||||
# TODO Remove the type ignore once this issue is fixed
|
||||
# https://github.com/python/mypy/issues/6019
|
||||
params.update( # type: ignore[call-arg]
|
||||
dedup="" if dedup_index == 0 else f"-{dedup_index}"
|
||||
)
|
||||
formatter = BetterStringFormatter()
|
||||
filename = formatter.format(self.name_format, **params).strip()
|
||||
yield self.parent / filename
|
||||
|
||||
|
||||
@singledispatch
|
||||
def extract_format_params(file: JubeatFile) -> FormatParameters:
|
||||
...
|
||||
|
||||
|
||||
@extract_format_params.register
|
||||
def extract_song_format_params(songfile: SongFile) -> FormatParameters:
|
||||
return FormatParameters(title=none_or(slugify, songfile.song.metadata.title) or "")
|
||||
|
||||
|
||||
@extract_format_params.register
|
||||
def extract_chart_format_params(chartfile: ChartFile) -> FormatParameters:
|
||||
return FormatParameters(
|
||||
title=none_or(slugify, chartfile.song.metadata.title) or "",
|
||||
difficulty=slugify(chartfile.difficulty),
|
||||
difficulty_index=str(DIFFICULTY_INDEX.get(chartfile.difficulty, 2)),
|
||||
difficulty_number=str(DIFFICULTY_NUMBER.get(chartfile.difficulty, 3)),
|
||||
dedup="" if dedup_index == 0 else f"-{dedup_index}",
|
||||
)
|
||||
|
||||
|
||||
|
@ -8,6 +8,7 @@ class Format(str, Enum):
|
||||
MEMON_LEGACY = "memon:legacy"
|
||||
MEMON_0_1_0 = "memon:v0.1.0"
|
||||
MEMON_0_2_0 = "memon:v0.2.0"
|
||||
MEMON_0_3_0 = "memon:v0.3.0"
|
||||
MONO_COLUMN = "mono-column"
|
||||
MEMO = "memo"
|
||||
MEMO_1 = "memo1"
|
||||
|
@ -1,50 +1,33 @@
|
||||
from typing import Dict
|
||||
|
||||
from . import jubeat_analyser, konami, malody, memon
|
||||
from .enum import Format
|
||||
from .jubeat_analyser import (
|
||||
dump_memo,
|
||||
dump_memo1,
|
||||
dump_memo2,
|
||||
dump_mono_column,
|
||||
load_memo,
|
||||
load_memo1,
|
||||
load_memo2,
|
||||
load_mono_column,
|
||||
)
|
||||
from .konami import dump_eve, dump_jbsq, load_eve, load_jbsq
|
||||
from .malody import dump_malody, load_malody
|
||||
from .memon import (
|
||||
dump_memon_0_1_0,
|
||||
dump_memon_0_2_0,
|
||||
dump_memon_legacy,
|
||||
load_memon_0_1_0,
|
||||
load_memon_0_2_0,
|
||||
load_memon_legacy,
|
||||
)
|
||||
from .typing import Dumper, Loader
|
||||
|
||||
LOADERS: Dict[Format, Loader] = {
|
||||
Format.EVE: load_eve,
|
||||
Format.JBSQ: load_jbsq,
|
||||
Format.MALODY: load_malody,
|
||||
Format.MEMON_LEGACY: load_memon_legacy,
|
||||
Format.MEMON_0_1_0: load_memon_0_1_0,
|
||||
Format.MEMON_0_2_0: load_memon_0_2_0,
|
||||
Format.MONO_COLUMN: load_mono_column,
|
||||
Format.MEMO: load_memo,
|
||||
Format.MEMO_1: load_memo1,
|
||||
Format.MEMO_2: load_memo2,
|
||||
Format.EVE: konami.load_eve,
|
||||
Format.JBSQ: konami.load_jbsq,
|
||||
Format.MALODY: malody.load_malody,
|
||||
Format.MEMON_LEGACY: memon.load_memon_legacy,
|
||||
Format.MEMON_0_1_0: memon.load_memon_0_1_0,
|
||||
Format.MEMON_0_2_0: memon.load_memon_0_2_0,
|
||||
Format.MEMON_0_3_0: memon.load_memon_0_3_0,
|
||||
Format.MONO_COLUMN: jubeat_analyser.load_mono_column,
|
||||
Format.MEMO: jubeat_analyser.load_memo,
|
||||
Format.MEMO_1: jubeat_analyser.load_memo1,
|
||||
Format.MEMO_2: jubeat_analyser.load_memo2,
|
||||
}
|
||||
|
||||
DUMPERS: Dict[Format, Dumper] = {
|
||||
Format.EVE: dump_eve,
|
||||
Format.JBSQ: dump_jbsq,
|
||||
Format.MALODY: dump_malody,
|
||||
Format.MEMON_LEGACY: dump_memon_legacy,
|
||||
Format.MEMON_0_1_0: dump_memon_0_1_0,
|
||||
Format.MEMON_0_2_0: dump_memon_0_2_0,
|
||||
Format.MONO_COLUMN: dump_mono_column,
|
||||
Format.MEMO: dump_memo,
|
||||
Format.MEMO_1: dump_memo1,
|
||||
Format.MEMO_2: dump_memo2,
|
||||
Format.EVE: konami.dump_eve,
|
||||
Format.JBSQ: konami.dump_jbsq,
|
||||
Format.MALODY: malody.dump_malody,
|
||||
Format.MEMON_LEGACY: memon.dump_memon_legacy,
|
||||
Format.MEMON_0_1_0: memon.dump_memon_0_1_0,
|
||||
Format.MEMON_0_2_0: memon.dump_memon_0_2_0,
|
||||
Format.MEMON_0_3_0: memon.dump_memon_0_3_0,
|
||||
Format.MONO_COLUMN: jubeat_analyser.dump_mono_column,
|
||||
Format.MEMO: jubeat_analyser.dump_memo,
|
||||
Format.MEMO_1: jubeat_analyser.dump_memo1,
|
||||
Format.MEMO_2: jubeat_analyser.dump_memo2,
|
||||
}
|
||||
|
@ -53,6 +53,8 @@ def recognize_memon_version(obj: dict) -> Format:
|
||||
return Format.MEMON_0_1_0
|
||||
elif version == "0.2.0":
|
||||
return Format.MEMON_0_2_0
|
||||
elif version == "0.3.0":
|
||||
return Format.MEMON_0_3_0
|
||||
else:
|
||||
raise ValueError(f"Unsupported memon version : {version}")
|
||||
|
||||
|
@ -210,7 +210,8 @@ def create_sections_from_chart(
|
||||
|
||||
header = sections[BeatsTime(0)].commands
|
||||
header["o"] = int(timing.beat_zero_offset * 1000)
|
||||
header["lev"] = Decimal(chart.level)
|
||||
if chart.level is not None:
|
||||
header["lev"] = Decimal(chart.level)
|
||||
header["dif"] = DIFFICULTY_NUMBER.get(difficulty, 3)
|
||||
if metadata.audio is not None:
|
||||
header["m"] = metadata.audio
|
||||
|
@ -299,7 +299,8 @@ def _dump_memo2_chart(
|
||||
file.write(f"// https://github.com/Stepland/jubeatools\n\n")
|
||||
|
||||
# Header
|
||||
file.write(dump_command("lev", Decimal(chart.level)) + "\n")
|
||||
if chart.level is not None:
|
||||
file.write(dump_command("lev", Decimal(chart.level)) + "\n")
|
||||
file.write(dump_command("dif", DIFFICULTY_NUMBER.get(difficulty, 1)) + "\n")
|
||||
if metadata.audio is not None:
|
||||
file.write(dump_command("m", metadata.audio) + "\n")
|
||||
|
@ -27,7 +27,9 @@ class FolderLoader(Protocol[T]):
|
||||
...
|
||||
|
||||
|
||||
def make_folder_loader(glob_pattern: str, file_loader: FileLoader) -> FolderLoader:
|
||||
def make_folder_loader(
|
||||
glob_pattern: str, file_loader: FileLoader[T]
|
||||
) -> FolderLoader[T]:
|
||||
def folder_loader(path: Path) -> Dict[Path, T]:
|
||||
files: Dict[Path, T] = {}
|
||||
if path.is_dir():
|
||||
|
@ -9,7 +9,7 @@ import simplejson as json
|
||||
|
||||
from jubeatools import song
|
||||
from jubeatools.formats import timemap
|
||||
from jubeatools.formats.load_tools import make_folder_loader
|
||||
from jubeatools.formats.load_tools import FolderLoader, make_folder_loader
|
||||
from jubeatools.utils import none_or
|
||||
|
||||
from . import schema as malody
|
||||
@ -26,7 +26,7 @@ def load_file(path: Path) -> Any:
|
||||
return json.load(f, use_decimal=True)
|
||||
|
||||
|
||||
load_folder = make_folder_loader("*.mc", load_file)
|
||||
load_folder: FolderLoader[Any] = make_folder_loader("*.mc", load_file)
|
||||
|
||||
|
||||
def load_malody_file(raw_dict: dict) -> song.Song:
|
||||
|
@ -8,11 +8,13 @@ parse than existing "memo-like" formats (memo, youbeat, etc ...).
|
||||
https://github.com/Stepland/memon
|
||||
"""
|
||||
|
||||
from .memon import (
|
||||
from .v0 import (
|
||||
dump_memon_0_1_0,
|
||||
dump_memon_0_2_0,
|
||||
dump_memon_0_3_0,
|
||||
dump_memon_legacy,
|
||||
load_memon_0_1_0,
|
||||
load_memon_0_2_0,
|
||||
load_memon_0_3_0,
|
||||
load_memon_legacy,
|
||||
)
|
||||
|
@ -1,36 +1,12 @@
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
|
||||
import hypothesis.strategies as st
|
||||
from hypothesis import given
|
||||
|
||||
from jubeatools import song
|
||||
from jubeatools.formats.typing import Dumper, Loader
|
||||
from jubeatools.formats.enum import Format
|
||||
from jubeatools.testutils import strategies as jbst
|
||||
|
||||
from . import (
|
||||
dump_memon_0_1_0,
|
||||
dump_memon_0_2_0,
|
||||
dump_memon_legacy,
|
||||
load_memon_0_1_0,
|
||||
load_memon_0_2_0,
|
||||
load_memon_legacy,
|
||||
)
|
||||
|
||||
|
||||
def dump_and_load(
|
||||
expected_song: song.Song, dump_function: Dumper, load_function: Loader
|
||||
) -> None:
|
||||
with tempfile.NamedTemporaryFile(mode="wb") as file:
|
||||
files = dump_function(expected_song, Path(file.name))
|
||||
assert len(files) == 1
|
||||
filename, contents = list(files.items())[0]
|
||||
file.write(contents)
|
||||
file.seek(0)
|
||||
actual_song = load_function(Path(file.name))
|
||||
|
||||
assert actual_song == expected_song
|
||||
from jubeatools.testutils.test_patterns import dump_and_load_then_compare
|
||||
|
||||
|
||||
@st.composite
|
||||
@ -56,6 +32,11 @@ def memon_legacy_compatible_song(draw: st.DrawFn) -> song.Song:
|
||||
diffs_strat=memon_diffs(),
|
||||
chart_strat=jbst.chart(timing_strat=st.none()),
|
||||
common_timing_strat=jbst.timing_info(with_bpm_changes=False),
|
||||
metadata_strat=jbst.metadata(
|
||||
text_strat=st.text(
|
||||
alphabet=st.characters(blacklist_categories=("Cc", "Cs")),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
random_song.metadata.preview = None
|
||||
@ -65,7 +46,7 @@ def memon_legacy_compatible_song(draw: st.DrawFn) -> song.Song:
|
||||
|
||||
@given(memon_legacy_compatible_song())
|
||||
def test_memon_legacy(song: song.Song) -> None:
|
||||
dump_and_load(song, dump_memon_legacy, load_memon_legacy)
|
||||
dump_and_load_then_compare(Format.MEMON_LEGACY, song)
|
||||
|
||||
|
||||
memon_0_1_0_compatible_song = memon_legacy_compatible_song
|
||||
@ -73,7 +54,7 @@ memon_0_1_0_compatible_song = memon_legacy_compatible_song
|
||||
|
||||
@given(memon_0_1_0_compatible_song())
|
||||
def test_memon_0_1_0(song: song.Song) -> None:
|
||||
dump_and_load(song, dump_memon_0_1_0, load_memon_0_1_0)
|
||||
dump_and_load_then_compare(Format.MEMON_0_1_0, song)
|
||||
|
||||
|
||||
@st.composite
|
||||
@ -84,6 +65,11 @@ def memon_0_2_0_compatible_song(draw: st.DrawFn) -> song.Song:
|
||||
diffs_strat=memon_diffs(),
|
||||
chart_strat=jbst.chart(timing_strat=st.none()),
|
||||
common_timing_strat=jbst.timing_info(with_bpm_changes=False),
|
||||
metadata_strat=jbst.metadata(
|
||||
text_strat=st.text(
|
||||
alphabet=st.characters(blacklist_categories=("Cc", "Cs")),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
random_song.metadata.preview_file = None
|
||||
@ -92,4 +78,25 @@ def memon_0_2_0_compatible_song(draw: st.DrawFn) -> song.Song:
|
||||
|
||||
@given(memon_0_2_0_compatible_song())
|
||||
def test_memon_0_2_0(song: song.Song) -> None:
|
||||
dump_and_load(song, dump_memon_0_2_0, load_memon_0_2_0)
|
||||
dump_and_load_then_compare(Format.MEMON_0_2_0, song)
|
||||
|
||||
|
||||
@st.composite
|
||||
def memon_0_3_0_compatible_song(draw: st.DrawFn) -> song.Song:
|
||||
return draw(
|
||||
jbst.song(
|
||||
diffs_strat=memon_diffs(),
|
||||
chart_strat=jbst.chart(timing_strat=st.none()),
|
||||
common_timing_strat=jbst.timing_info(with_bpm_changes=False),
|
||||
metadata_strat=jbst.metadata(
|
||||
text_strat=st.text(
|
||||
alphabet=st.characters(blacklist_categories=("Cc", "Cs")),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@given(memon_0_3_0_compatible_song())
|
||||
def test_memon_0_3_0(song: song.Song) -> None:
|
||||
dump_and_load_then_compare(Format.MEMON_0_3_0, song)
|
||||
|
@ -1,7 +1,8 @@
|
||||
from functools import reduce
|
||||
from io import StringIO
|
||||
from itertools import chain
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Union
|
||||
from typing import Any, Callable, Dict, List, Union
|
||||
|
||||
import simplejson as json
|
||||
from marshmallow import (
|
||||
@ -15,7 +16,11 @@ from marshmallow import (
|
||||
from multidict import MultiDict
|
||||
|
||||
from jubeatools import song as jbt
|
||||
from jubeatools.utils import lcm
|
||||
from jubeatools.formats.dump_tools import FileNameFormat
|
||||
from jubeatools.formats.filetypes import SongFile
|
||||
from jubeatools.formats.load_tools import FolderLoader, make_folder_loader
|
||||
from jubeatools.formats.typing import Dumper, Loader
|
||||
from jubeatools.utils import lcm, none_or
|
||||
|
||||
# v0.x.x long note value :
|
||||
#
|
||||
@ -102,6 +107,12 @@ class MemonMetadata_0_2_0(MemonMetadata_0_1_0):
|
||||
preview = fields.Nested(MemonPreview)
|
||||
|
||||
|
||||
class MemonMetadata_0_3_0(MemonMetadata_0_2_0):
|
||||
audio = fields.String(required=False, data_key="music path")
|
||||
cover = fields.String(required=False, data_key="album cover path")
|
||||
preview_path = fields.String(data_key="preview path")
|
||||
|
||||
|
||||
class Memon_legacy(StrictSchema):
|
||||
metadata = fields.Nested(MemonMetadata_legacy, required=True)
|
||||
data = fields.Nested(MemonChart_legacy, required=True, many=True)
|
||||
@ -123,15 +134,39 @@ class Memon_0_2_0(StrictSchema):
|
||||
)
|
||||
|
||||
|
||||
def _load_raw_memon(file: Path) -> Dict[str, Any]:
|
||||
with open(file) as f:
|
||||
res = json.load(f, use_decimal=True)
|
||||
if not isinstance(res, dict):
|
||||
class Memon_0_3_0(StrictSchema):
|
||||
version = fields.String(required=True, validate=validate.OneOf(["0.3.0"]))
|
||||
metadata = fields.Nested(MemonMetadata_0_3_0, required=True)
|
||||
data = fields.Dict(
|
||||
keys=fields.String(), values=fields.Nested(MemonChart_0_1_0), required=True
|
||||
)
|
||||
|
||||
|
||||
def _load_raw_memon(path: Path) -> Any:
|
||||
with path.open() as f:
|
||||
return json.load(f, use_decimal=True)
|
||||
|
||||
|
||||
load_folder: FolderLoader[Any] = make_folder_loader("*.memon", _load_raw_memon)
|
||||
|
||||
|
||||
def make_folder_loader_with_optional_merge(
|
||||
memon_loader: Callable[[Any], jbt.Song]
|
||||
) -> Loader:
|
||||
def load(path: Path, merge: bool = False, **kwargs: Any) -> jbt.Song:
|
||||
files = load_folder(path)
|
||||
if not merge and len(files) > 1:
|
||||
raise ValueError(
|
||||
"JSON file does not represent a valid memon file : "
|
||||
"The top level of a memon file should be a JSON Object"
|
||||
"Multiple .memon files were found in the given folder, "
|
||||
"use the --merge option if you want to make a single memon file "
|
||||
"out of several that each containt a different chart (or set of "
|
||||
"charts) for the same song"
|
||||
)
|
||||
return res
|
||||
|
||||
charts = [memon_loader(d) for d in files.values()]
|
||||
return reduce(jbt.Song.merge, charts)
|
||||
|
||||
return load
|
||||
|
||||
|
||||
def _load_memon_note_v0(
|
||||
@ -149,8 +184,7 @@ def _load_memon_note_v0(
|
||||
return jbt.TapNote(time, position)
|
||||
|
||||
|
||||
def load_memon_legacy(path: Path, **kwargs: Any) -> jbt.Song:
|
||||
raw_memon = _load_raw_memon(path)
|
||||
def _load_memon_legacy(raw_memon: Any) -> jbt.Song:
|
||||
schema = Memon_legacy()
|
||||
memon = schema.load(raw_memon)
|
||||
metadata = jbt.Metadata(
|
||||
@ -179,8 +213,10 @@ def load_memon_legacy(path: Path, **kwargs: Any) -> jbt.Song:
|
||||
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
|
||||
|
||||
|
||||
def load_memon_0_1_0(path: Path, **kwargs: Any) -> jbt.Song:
|
||||
raw_memon = _load_raw_memon(path)
|
||||
load_memon_legacy = make_folder_loader_with_optional_merge(_load_memon_legacy)
|
||||
|
||||
|
||||
def _load_memon_0_1_0(raw_memon: Any) -> jbt.Song:
|
||||
schema = Memon_0_1_0()
|
||||
memon = schema.load(raw_memon)
|
||||
metadata = jbt.Metadata(
|
||||
@ -209,8 +245,10 @@ def load_memon_0_1_0(path: Path, **kwargs: Any) -> jbt.Song:
|
||||
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
|
||||
|
||||
|
||||
def load_memon_0_2_0(path: Path, **kwargs: Any) -> jbt.Song:
|
||||
raw_memon = _load_raw_memon(path)
|
||||
load_memon_0_1_0 = make_folder_loader_with_optional_merge(_load_memon_0_1_0)
|
||||
|
||||
|
||||
def _load_memon_0_2_0(raw_memon: Any) -> jbt.Song:
|
||||
schema = Memon_0_2_0()
|
||||
memon = schema.load(raw_memon)
|
||||
preview = None
|
||||
@ -246,6 +284,49 @@ def load_memon_0_2_0(path: Path, **kwargs: Any) -> jbt.Song:
|
||||
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
|
||||
|
||||
|
||||
load_memon_0_2_0 = make_folder_loader_with_optional_merge(_load_memon_0_2_0)
|
||||
|
||||
|
||||
def _load_memon_0_3_0(raw_memon: Any) -> jbt.Song:
|
||||
schema = Memon_0_3_0()
|
||||
memon = schema.load(raw_memon)
|
||||
preview = None
|
||||
if "preview" in memon["metadata"]:
|
||||
start = memon["metadata"]["preview"]["position"]
|
||||
length = memon["metadata"]["preview"]["length"]
|
||||
preview = jbt.Preview(start, length)
|
||||
|
||||
metadata = jbt.Metadata(
|
||||
title=memon["metadata"]["title"],
|
||||
artist=memon["metadata"]["artist"],
|
||||
audio=none_or(Path, memon["metadata"].get("audio")),
|
||||
cover=none_or(Path, memon["metadata"].get("cover")),
|
||||
preview=preview,
|
||||
preview_file=none_or(Path, memon["metadata"].get("preview_path")),
|
||||
)
|
||||
common_timing = jbt.Timing(
|
||||
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=memon["metadata"]["BPM"])],
|
||||
beat_zero_offset=jbt.SecondsTime(-memon["metadata"]["offset"]),
|
||||
)
|
||||
charts: MultiDict[jbt.Chart] = MultiDict()
|
||||
for difficulty, memon_chart in memon["data"].items():
|
||||
charts.add(
|
||||
difficulty,
|
||||
jbt.Chart(
|
||||
level=memon_chart["level"],
|
||||
notes=[
|
||||
_load_memon_note_v0(note, memon_chart["resolution"])
|
||||
for note in memon_chart["notes"]
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
|
||||
|
||||
|
||||
load_memon_0_3_0 = make_folder_loader_with_optional_merge(_load_memon_0_3_0)
|
||||
|
||||
|
||||
def _long_note_tail_value_v0(note: jbt.LongNote) -> int:
|
||||
dx = note.tail_tip.x - note.position.x
|
||||
dy = note.tail_tip.y - note.position.y
|
||||
@ -267,7 +348,6 @@ def _get_timing(song: jbt.Song) -> jbt.Timing:
|
||||
|
||||
|
||||
def _raise_if_unfit_for_v0(song: jbt.Song, version: str) -> None:
|
||||
|
||||
"""Raises an exception if the Song object is ill-formed or contains information
|
||||
that cannot be represented in a memon v0.x.y file (includes legacy)"""
|
||||
|
||||
@ -347,8 +427,7 @@ def _dump_memon_note_v0(
|
||||
return memon_note
|
||||
|
||||
|
||||
def dump_memon_legacy(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, bytes]:
|
||||
|
||||
def _dump_memon_legacy(song: jbt.Song) -> SongFile:
|
||||
_raise_if_unfit_for_v0(song, "legacy")
|
||||
timing = _get_timing(song)
|
||||
|
||||
@ -379,16 +458,23 @@ def dump_memon_legacy(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path,
|
||||
}
|
||||
)
|
||||
|
||||
if path.is_dir():
|
||||
filepath = path / f"{song.metadata.title}.memon"
|
||||
else:
|
||||
filepath = path
|
||||
|
||||
return {filepath: _dump_to_json(memon)}
|
||||
return SongFile(contents=_dump_to_json(memon), song=song)
|
||||
|
||||
|
||||
def dump_memon_0_1_0(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, bytes]:
|
||||
def make_memon_dumper(internal_dumper: Callable[[jbt.Song], SongFile]) -> Dumper:
|
||||
def dump(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, bytes]:
|
||||
name_format = FileNameFormat(Path("{title}.memon"), suggestion=path)
|
||||
songfile = internal_dumper(song)
|
||||
filepath = name_format.available_filename_for(songfile)
|
||||
return {filepath: songfile.contents}
|
||||
|
||||
return dump
|
||||
|
||||
|
||||
dump_memon_legacy = make_memon_dumper(_dump_memon_legacy)
|
||||
|
||||
|
||||
def _dump_memon_0_1_0(song: jbt.Song) -> SongFile:
|
||||
_raise_if_unfit_for_v0(song, "v0.1.0")
|
||||
timing = _get_timing(song)
|
||||
|
||||
@ -415,15 +501,13 @@ def dump_memon_0_1_0(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, b
|
||||
],
|
||||
}
|
||||
|
||||
if path.is_dir():
|
||||
filepath = path / f"{song.metadata.title}.memon"
|
||||
else:
|
||||
filepath = path
|
||||
|
||||
return {filepath: _dump_to_json(memon)}
|
||||
return SongFile(contents=_dump_to_json(memon), song=song)
|
||||
|
||||
|
||||
def dump_memon_0_2_0(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, bytes]:
|
||||
dump_memon_0_1_0 = make_memon_dumper(_dump_memon_0_1_0)
|
||||
|
||||
|
||||
def _dump_memon_0_2_0(song: jbt.Song) -> SongFile:
|
||||
|
||||
_raise_if_unfit_for_v0(song, "v0.2.0")
|
||||
timing = _get_timing(song)
|
||||
@ -458,9 +542,55 @@ def dump_memon_0_2_0(song: jbt.Song, path: Path, **kwargs: dict) -> Dict[Path, b
|
||||
],
|
||||
}
|
||||
|
||||
if path.is_dir():
|
||||
filepath = path / f"{song.metadata.title}.memon"
|
||||
else:
|
||||
filepath = path
|
||||
return SongFile(contents=_dump_to_json(memon), song=song)
|
||||
|
||||
return {filepath: _dump_to_json(memon)}
|
||||
|
||||
dump_memon_0_2_0 = make_memon_dumper(_dump_memon_0_2_0)
|
||||
|
||||
|
||||
def _dump_memon_0_3_0(song: jbt.Song) -> SongFile:
|
||||
|
||||
_raise_if_unfit_for_v0(song, "v0.3.0")
|
||||
timing = _get_timing(song)
|
||||
|
||||
memon: Dict[str, Any] = {
|
||||
"version": "0.3.0",
|
||||
"metadata": {
|
||||
"song title": song.metadata.title,
|
||||
"artist": song.metadata.artist,
|
||||
"BPM": timing.events[0].BPM,
|
||||
"offset": -timing.beat_zero_offset,
|
||||
},
|
||||
"data": {},
|
||||
}
|
||||
|
||||
if song.metadata.audio is not None:
|
||||
memon["metadata"]["music path"] = str(song.metadata.audio)
|
||||
|
||||
if song.metadata.cover is not None:
|
||||
memon["metadata"]["album cover path"] = str(song.metadata.cover)
|
||||
|
||||
if song.metadata.preview is not None:
|
||||
memon["metadata"]["preview"] = {
|
||||
"position": song.metadata.preview.start,
|
||||
"length": song.metadata.preview.length,
|
||||
}
|
||||
|
||||
if song.metadata.preview_file is not None:
|
||||
memon["metadata"]["preview path"] = str(song.metadata.preview_file)
|
||||
|
||||
for difficulty, chart in song.charts.items():
|
||||
resolution = _compute_resolution(chart.notes)
|
||||
memon["data"][difficulty] = {
|
||||
"level": chart.level,
|
||||
"resolution": resolution,
|
||||
"notes": [
|
||||
_dump_memon_note_v0(note, resolution)
|
||||
for note in sorted(set(chart.notes), key=lambda n: (n.time, n.position))
|
||||
],
|
||||
}
|
||||
|
||||
return SongFile(contents=_dump_to_json(memon), song=song)
|
||||
|
||||
|
||||
dump_memon_0_3_0 = make_memon_dumper(_dump_memon_0_3_0)
|
@ -1,17 +1,15 @@
|
||||
from decimal import Decimal
|
||||
from fractions import Fraction
|
||||
|
||||
import pytest
|
||||
|
||||
from ..load_tools import round_beats
|
||||
|
||||
|
||||
@pytest.mark.parametrize("numerator", range(240))
|
||||
def test_fraction_recovery_after_rounding_to_three_decimals(numerator: int) -> None:
|
||||
fraction = Fraction(numerator, 240)
|
||||
decimal = numerator / Decimal(240)
|
||||
rounded = round(decimal, 3)
|
||||
text_form = str(rounded)
|
||||
re_parsed_decimal = Decimal(text_form)
|
||||
result = round_beats(re_parsed_decimal)
|
||||
assert fraction == result
|
||||
def test_fraction_recovery_after_rounding_to_three_decimals() -> None:
|
||||
for numerator in range(240):
|
||||
fraction = Fraction(numerator, 240)
|
||||
decimal = numerator / Decimal(240)
|
||||
rounded = round(decimal, 3)
|
||||
text_form = str(rounded)
|
||||
re_parsed_decimal = Decimal(text_form)
|
||||
result = round_beats(re_parsed_decimal)
|
||||
assert fraction == result
|
||||
|
@ -180,7 +180,7 @@ class Timing:
|
||||
|
||||
@dataclass
|
||||
class Chart:
|
||||
level: Decimal
|
||||
level: Optional[Decimal]
|
||||
timing: Optional[Timing] = None
|
||||
notes: List[Union[TapNote, LongNote]] = field(default_factory=list)
|
||||
|
||||
|
@ -20,7 +20,7 @@ def open_temp_dir() -> Iterator[Path]:
|
||||
def dump_and_load_then_compare(
|
||||
format_: Format,
|
||||
song: song.Song,
|
||||
bytes_decoder: Callable[[bytes], str],
|
||||
bytes_decoder: Callable[[bytes], str] = lambda b: b.decode("utf-8"),
|
||||
temp_path: Callable[[], ContextManager[Path]] = open_temp_dir,
|
||||
load_options: Optional[dict] = None,
|
||||
dump_options: Optional[dict] = None,
|
||||
|
14
poetry.lock
generated
14
poetry.lock
generated
@ -498,6 +498,14 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "types-toml"
|
||||
version = "0.10.1"
|
||||
description = "Typing stubs for toml"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "3.10.0.2"
|
||||
@ -521,7 +529,7 @@ typing-extensions = ">=3.7.4"
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "667e5000f1e328f2d07a86946ad1423912c875c0abcc7efb97a72aec38fdb916"
|
||||
content-hash = "025ff93ec3b033c89bdaaeab7905dc10ff450ed70695bcbd863a10e6af6fa9ea"
|
||||
|
||||
[metadata.files]
|
||||
atomicwrites = [
|
||||
@ -864,6 +872,10 @@ types-simplejson = [
|
||||
{file = "types-simplejson-3.17.1.tar.gz", hash = "sha256:310ef5addfe97c20b9f2cac85079cbab95fd123c2c9a5c6b99856075d6b48211"},
|
||||
{file = "types_simplejson-3.17.1-py3-none-any.whl", hash = "sha256:7b5ac5549f5269f25e5e932d6f4e96ea8397369d6031300161cd1fda484e2534"},
|
||||
]
|
||||
types-toml = [
|
||||
{file = "types-toml-0.10.1.tar.gz", hash = "sha256:5c1f8f8d57692397c8f902bf6b4d913a0952235db7db17d2908cc110e70610cb"},
|
||||
{file = "types_toml-0.10.1-py3-none-any.whl", hash = "sha256:8cdfd2b7c89bed703158b042dd5cf04255dae77096db66f4a12ca0a93ccb07a5"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
|
||||
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
|
||||
|
@ -32,6 +32,7 @@ toml = "^0.10.2"
|
||||
flake8 = "^3.9.1"
|
||||
autoimport = "^0.7.0"
|
||||
types-simplejson = "^3.17.1"
|
||||
types-toml = "^0.10.1"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
jubeatools = 'jubeatools.cli.cli:convert'
|
||||
|
Loading…
Reference in New Issue
Block a user