Add marshmallow models for memon
This commit is contained in:
parent
cbb902de84
commit
3a0110ea07
@ -8,13 +8,13 @@ parse than existing "memo-like" formats (memo, youbeat, etc ...).
|
||||
https://github.com/Stepland/memon
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from path import Path
|
||||
from typing import Mapping, IO, Iterable, Tuple, Any, Dict, Union, List
|
||||
from typing import IO, Iterable, Tuple, Any, Dict, Union, List
|
||||
from io import BytesIO
|
||||
from itertools import chain
|
||||
|
||||
from path import Path
|
||||
import simplejson as json
|
||||
from marshmallow import Schema, fields, RAISE, validate, validates_schema, ValidationError
|
||||
|
||||
from jubeatools.song import Song, BPMChange, TapNote, LongNote
|
||||
from jubeatools.utils import lcm
|
||||
@ -29,7 +29,7 @@ from jubeatools.utils import lcm
|
||||
# 6
|
||||
# 10
|
||||
|
||||
LONG_NOTE_VALUE_V0 = {
|
||||
X_Y_OFFSET_TO_P_VALUE = {
|
||||
(0, -1): 0,
|
||||
(0, -2): 4,
|
||||
(0, -3): 8,
|
||||
@ -44,9 +44,101 @@ LONG_NOTE_VALUE_V0 = {
|
||||
(-3, 0): 11
|
||||
}
|
||||
|
||||
P_VALUE_TO_X_Y_OFFSET = { v: k for k, v in X_Y_OFFSET_TO_P_VALUE.items() }
|
||||
|
||||
|
||||
class StrictSchema(Schema):
|
||||
class Meta:
|
||||
unknown = RAISE
|
||||
|
||||
|
||||
class MemonNote(StrictSchema):
|
||||
n = fields.Integer(required=True, validate=validate.Range(min=0, max=15))
|
||||
t = fields.Integer(required=True, validate=validate.Range(min=0))
|
||||
l = fields.Integer(required=True, validate=validate.Range(min=0))
|
||||
p = fields.Integer(required=True, validate=validate.Range(min=0, max=11))
|
||||
|
||||
@validates_schema
|
||||
def validate_tail_tip_position(self, data, **kwargs):
|
||||
if data["l"] > 0:
|
||||
x = data["n"] % 4
|
||||
y = data["n"] // 4
|
||||
dx, dy = P_VALUE_TO_X_Y_OFFSET[data["p"]]
|
||||
if (not (0 <= x + dx < 4 and 0 <= y + dy < 4)):
|
||||
raise ValidationError("Invalid tail position : {data}")
|
||||
|
||||
|
||||
class MemonChart_0_1_0(StrictSchema):
|
||||
level = fields.Integer(required=True)
|
||||
resolution = fields.Integer(required=True, validate=validate.Range(min=1))
|
||||
notes = fields.Nested(MemonNote, many=True)
|
||||
|
||||
|
||||
class MemonChart_legacy(MemonChart_0_1_0):
|
||||
dif_name = fields.String(required=True)
|
||||
|
||||
|
||||
class MemonMetadata_legacy(StrictSchema):
|
||||
title = fields.String(required=True, data_key="song title")
|
||||
artist = fields.String(required=True)
|
||||
audio = fields.String(required=True, data_key="music path")
|
||||
cover = fields.String(required=True, data_key="jacket path")
|
||||
BPM = fields.Decimal(required=True, validate=validate.Range(min=0, min_inclusive=False))
|
||||
offset = fields.Decimal(required=True)
|
||||
|
||||
|
||||
class MemonMetadata_0_1_0(MemonMetadata_legacy):
|
||||
cover = fields.String(required=True, data_key="album cover path")
|
||||
|
||||
|
||||
class MemonPreview(StrictSchema):
|
||||
position = fields.Decimal(required=True, validate=validate.Range(min=0))
|
||||
length = fields.Decimal(required=True, validate=validate.Range(min=0, min_inclusive=False))
|
||||
|
||||
|
||||
class MemonMetadata_0_2_0(MemonMetadata_0_1_0):
|
||||
preview = fields.Nested(MemonPreview)
|
||||
|
||||
|
||||
class Memon_legacy(StrictSchema):
|
||||
metadata = fields.Nested(MemonMetadata_legacy, required=True)
|
||||
data = fields.Nested(MemonChart_legacy, required=True, many=True)
|
||||
|
||||
|
||||
class Memon_0_1_0(StrictSchema):
|
||||
version = fields.String(required=True, validate=validate.OneOf(["0.1.0"]))
|
||||
metadata = fields.Nested(MemonMetadata_0_1_0, required=True)
|
||||
data = fields.Dict(keys=fields.String(), values=MemonChart_0_1_0(), required=True)
|
||||
|
||||
|
||||
class Memon_0_2_0(StrictSchema):
|
||||
version = fields.String(required=True, validate=validate.OneOf(["0.2.0"]))
|
||||
metadata = fields.Nested(MemonMetadata_0_2_0, required=True)
|
||||
data = fields.Dict(keys=fields.String(), values=MemonChart_0_1_0(), required=True)
|
||||
|
||||
|
||||
def _search_and_load(file_or_folder: Path) -> Any:
|
||||
|
||||
"""If given a folder, search for a single .memon file then json.load it
|
||||
If given a file, just json.load it"""
|
||||
|
||||
if file_or_folder.isdir():
|
||||
memon_files = file_or_folder.files("*.memon")
|
||||
if len(memon_files) > 1:
|
||||
raise ValueError(f"Multiple memon files found in {file_or_folder}")
|
||||
elif len(memon_files) == 0:
|
||||
raise ValueError(f"No memon file found in {file_or_folder}")
|
||||
file_path = memon_files[0]
|
||||
else:
|
||||
file_path = file_or_folder
|
||||
|
||||
return json.load(open(file_path), use_decimal=True)
|
||||
|
||||
|
||||
def load_memon_legacy(file_or_folder: Path) -> Song:
|
||||
...
|
||||
memon = _search_and_load(file_or_folder)
|
||||
|
||||
|
||||
|
||||
|
||||
def load_memon_0_1_0(file_or_folder: Path) -> Song:
|
||||
@ -61,10 +153,11 @@ def _long_note_tail_value_v0(note: LongNote) -> int:
|
||||
dx = note.tail_tip.x - note.position.x
|
||||
dy = note.tail_tip.y - note.position.y
|
||||
try:
|
||||
return LONG_NOTE_VALUE_V0[dx, dy]
|
||||
return X_Y_OFFSET_TO_P_VALUE[dx, dy]
|
||||
except KeyError:
|
||||
raise ValueError(f"memon cannot represent a long note with its tail starting ({dx}, {dy}) away from the note") from None
|
||||
|
||||
|
||||
def check_representable_in_v0(song: Song, version: str) -> None:
|
||||
|
||||
"""Raises an exception if the Song object is ill-formed or contains information
|
||||
@ -114,6 +207,7 @@ def _compute_resolution(notes: List[Union[TapNote, LongNote]]) -> int:
|
||||
|
||||
|
||||
def _iter_dump_notes_v0(resolution: int, notes: List[Union[TapNote, LongNote]]) -> Iterable[Dict[str, int]]:
|
||||
"""Iterable that converts notes into the {n, t, l, p} form"""
|
||||
for note in sorted(set(notes), key=lambda n: (n.time, n.position)):
|
||||
memon_note = {
|
||||
"n": note.index,
|
||||
@ -132,7 +226,6 @@ def dump_memon_legacy(song: Song) -> Iterable[Tuple[Any, IO]]:
|
||||
|
||||
check_representable_in_v0(song, "legacy")
|
||||
|
||||
# JSON object preparation
|
||||
memon = {
|
||||
"metadata": {
|
||||
"song title": song.metadata.title,
|
||||
@ -160,7 +253,6 @@ def dump_memon_0_1_0(song: Song, folder: Path) -> None:
|
||||
|
||||
check_representable_in_v0(song, "legacy")
|
||||
|
||||
# JSON object preparation
|
||||
memon = {
|
||||
"version": "0.1.0",
|
||||
"metadata": {
|
||||
@ -188,7 +280,6 @@ def dump_memon_0_2_0(song: Song, folder: Path) -> None:
|
||||
|
||||
check_representable_in_v0(song, "legacy")
|
||||
|
||||
# JSON object preparation
|
||||
memon = {
|
||||
"version": "0.2.0",
|
||||
"metadata": {
|
||||
@ -198,13 +289,16 @@ def dump_memon_0_2_0(song: Song, folder: Path) -> None:
|
||||
"album cover path": str(song.metadata.cover),
|
||||
"BPM": song.global_timing.events[0].BPM,
|
||||
"offset": song.global_timing.beat_zero_offset,
|
||||
"preview" : {
|
||||
"position": song.metadata.preview_start,
|
||||
"length": song.metadata.preview_length,
|
||||
}
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
|
||||
if song.metadata.preview_length != 0:
|
||||
memon["metadata"]["preview"] = {
|
||||
"position": song.metadata.preview_start,
|
||||
"length": song.metadata.preview_length,
|
||||
}
|
||||
|
||||
for difficulty, chart in song.charts.items():
|
||||
resolution = _compute_resolution(chart.notes)
|
||||
memon["data"][difficulty] = {
|
||||
|
20
poetry.lock
generated
20
poetry.lock
generated
@ -66,6 +66,20 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "0.4.3"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
|
||||
name = "marshmallow"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "3.6.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest", "pytz", "simplejson", "mypy (0.770)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)", "tox"]
|
||||
docs = ["sphinx (3.0.3)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"]
|
||||
lint = ["mypy (0.770)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"]
|
||||
tests = ["pytest", "pytz", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "More routines for operating on iterables, beyond itertools"
|
||||
@ -223,7 +237,7 @@ python-versions = "*"
|
||||
version = "0.1.9"
|
||||
|
||||
[metadata]
|
||||
content-hash = "4c6754363fc490de2f3c7af8747cd102854de6437c41de5612bc8c1f14cb7cd9"
|
||||
content-hash = "b55c6b2244d11c0356dc49e32615244f2b964ac0b903d8613a5e0e453557643f"
|
||||
python-versions = "^3.8"
|
||||
|
||||
[metadata.files]
|
||||
@ -251,6 +265,10 @@ colorama = [
|
||||
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
|
||||
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
|
||||
]
|
||||
marshmallow = [
|
||||
{file = "marshmallow-3.6.0-py2.py3-none-any.whl", hash = "sha256:f88fe96434b1f0f476d54224d59333eba8ca1a203a2695683c1855675c4049a7"},
|
||||
{file = "marshmallow-3.6.0.tar.gz", hash = "sha256:c2673233aa21dde264b84349dc2fd1dce5f30ed724a0a00e75426734de5b84ab"},
|
||||
]
|
||||
more-itertools = [
|
||||
{file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"},
|
||||
{file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"},
|
||||
|
@ -11,6 +11,7 @@ multidict = "^4.7.6"
|
||||
click = "^7.1.2"
|
||||
path = "^14.0.1"
|
||||
simplejson = "^3.17.0"
|
||||
marshmallow = "^3.6.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^5.2"
|
||||
|
Loading…
x
Reference in New Issue
Block a user