1
0
mirror of synced 2024-12-04 03:07:16 +01:00

Add support for memon 0.3.0

This commit is contained in:
Stepland 2021-12-22 01:40:53 +01:00
parent 93ada9fd31
commit 2e4b5978bd
20 changed files with 336 additions and 165 deletions

View File

@ -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

View File

@ -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

View File

@ -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 | ✔️ | ✔️ |

View File

@ -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.

View File

@ -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}",
)

View File

@ -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"

View File

@ -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,
}

View File

@ -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}")

View File

@ -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

View File

@ -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")

View File

@ -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():

View File

@ -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:

View File

@ -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,
)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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
View File

@ -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"},

View File

@ -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'