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
|
https://github.com/Stepland/memon
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import warnings
|
from typing import IO, Iterable, Tuple, Any, Dict, Union, List
|
||||||
from path import Path
|
|
||||||
from typing import Mapping, IO, Iterable, Tuple, Any, Dict, Union, List
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
|
||||||
|
from path import Path
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
|
from marshmallow import Schema, fields, RAISE, validate, validates_schema, ValidationError
|
||||||
|
|
||||||
from jubeatools.song import Song, BPMChange, TapNote, LongNote
|
from jubeatools.song import Song, BPMChange, TapNote, LongNote
|
||||||
from jubeatools.utils import lcm
|
from jubeatools.utils import lcm
|
||||||
@ -29,7 +29,7 @@ from jubeatools.utils import lcm
|
|||||||
# 6
|
# 6
|
||||||
# 10
|
# 10
|
||||||
|
|
||||||
LONG_NOTE_VALUE_V0 = {
|
X_Y_OFFSET_TO_P_VALUE = {
|
||||||
(0, -1): 0,
|
(0, -1): 0,
|
||||||
(0, -2): 4,
|
(0, -2): 4,
|
||||||
(0, -3): 8,
|
(0, -3): 8,
|
||||||
@ -44,9 +44,101 @@ LONG_NOTE_VALUE_V0 = {
|
|||||||
(-3, 0): 11
|
(-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:
|
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:
|
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
|
dx = note.tail_tip.x - note.position.x
|
||||||
dy = note.tail_tip.y - note.position.y
|
dy = note.tail_tip.y - note.position.y
|
||||||
try:
|
try:
|
||||||
return LONG_NOTE_VALUE_V0[dx, dy]
|
return X_Y_OFFSET_TO_P_VALUE[dx, dy]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(f"memon cannot represent a long note with its tail starting ({dx}, {dy}) away from the note") from None
|
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:
|
def check_representable_in_v0(song: Song, version: str) -> None:
|
||||||
|
|
||||||
"""Raises an exception if the Song object is ill-formed or contains information
|
"""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]]:
|
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)):
|
for note in sorted(set(notes), key=lambda n: (n.time, n.position)):
|
||||||
memon_note = {
|
memon_note = {
|
||||||
"n": note.index,
|
"n": note.index,
|
||||||
@ -132,7 +226,6 @@ def dump_memon_legacy(song: Song) -> Iterable[Tuple[Any, IO]]:
|
|||||||
|
|
||||||
check_representable_in_v0(song, "legacy")
|
check_representable_in_v0(song, "legacy")
|
||||||
|
|
||||||
# JSON object preparation
|
|
||||||
memon = {
|
memon = {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"song title": song.metadata.title,
|
"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")
|
check_representable_in_v0(song, "legacy")
|
||||||
|
|
||||||
# JSON object preparation
|
|
||||||
memon = {
|
memon = {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@ -188,7 +280,6 @@ def dump_memon_0_2_0(song: Song, folder: Path) -> None:
|
|||||||
|
|
||||||
check_representable_in_v0(song, "legacy")
|
check_representable_in_v0(song, "legacy")
|
||||||
|
|
||||||
# JSON object preparation
|
|
||||||
memon = {
|
memon = {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@ -198,13 +289,16 @@ def dump_memon_0_2_0(song: Song, folder: Path) -> None:
|
|||||||
"album cover path": str(song.metadata.cover),
|
"album cover path": str(song.metadata.cover),
|
||||||
"BPM": song.global_timing.events[0].BPM,
|
"BPM": song.global_timing.events[0].BPM,
|
||||||
"offset": song.global_timing.beat_zero_offset,
|
"offset": song.global_timing.beat_zero_offset,
|
||||||
"preview" : {
|
|
||||||
"position": song.metadata.preview_start,
|
|
||||||
"length": song.metadata.preview_length,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"data": {}
|
"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():
|
for difficulty, chart in song.charts.items():
|
||||||
resolution = _compute_resolution(chart.notes)
|
resolution = _compute_resolution(chart.notes)
|
||||||
memon["data"][difficulty] = {
|
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.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
version = "0.4.3"
|
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]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "More routines for operating on iterables, beyond itertools"
|
description = "More routines for operating on iterables, beyond itertools"
|
||||||
@ -223,7 +237,7 @@ python-versions = "*"
|
|||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "4c6754363fc490de2f3c7af8747cd102854de6437c41de5612bc8c1f14cb7cd9"
|
content-hash = "b55c6b2244d11c0356dc49e32615244f2b964ac0b903d8613a5e0e453557643f"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
|
|
||||||
[metadata.files]
|
[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-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
|
||||||
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
|
{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 = [
|
more-itertools = [
|
||||||
{file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"},
|
{file = "more-itertools-8.3.0.tar.gz", hash = "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be"},
|
||||||
{file = "more_itertools-8.3.0-py3-none-any.whl", hash = "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"},
|
{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"
|
click = "^7.1.2"
|
||||||
path = "^14.0.1"
|
path = "^14.0.1"
|
||||||
simplejson = "^3.17.0"
|
simplejson = "^3.17.0"
|
||||||
|
marshmallow = "^3.6.0"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^5.2"
|
pytest = "^5.2"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user