1
0
mirror of synced 2024-12-03 10:47:17 +01:00

Merge pull request #21 from Stepland/more-stuff

More stuff
This commit is contained in:
Stepland 2021-12-28 16:57:06 +01:00 committed by GitHub
commit 587e68ab3e
34 changed files with 313 additions and 207 deletions

View File

@ -1,10 +1,20 @@
# v1.4.0
## Added
- Jubeatools can now handle HAKUs, in the following formats :
- Jubeatools can now handle HAKUs in the following formats :
- [memon:v1.0.0]
- [memon] 🎉 inital support for v1.0.0 !
- [eve]
- [jbsq]
- [memon]
- 🎉 inital support for v1.0.0 !
- `--merge` option allows for several memon files to be merged when
jubeatools is called on a folder
## Changed
- Improved the merging procedure for song objects
- Re-enable calling the CLI on a folder, this was disabled for some reason ?
- The song class now uses a regular dict to map difficuty names to chart
objects, dissalowing files with duplicate difficulties (`memon:legacy` was the
only format that *technically* supported this anyway, I conscider it an edge
case not really worth handling)
# v1.3.0
## Added

View File

@ -5,21 +5,23 @@ from typing import Any, Dict, Optional
import click
from jubeatools.formats import DUMPERS, LOADERS
from jubeatools.formats.enum import Format
from jubeatools.formats import DUMPERS, LOADERS, Format
from jubeatools.formats.guess import guess_format
from .helpers import dumper_option, loader_option
@click.command()
@click.argument("src", type=click.Path(exists=True, dir_okay=False))
@click.argument("src", type=click.Path(exists=True, dir_okay=True))
@click.argument("dst", type=click.Path())
@click.option(
"--input-format",
"input_format",
type=click.Choice(list(f._value_ for f in LOADERS.keys())),
help="Input file format",
help=(
"Force jubeatools to read the input file/folder as the given format."
"If this option is not used jubeatools will try to guess the format"
),
)
@click.option(
"-f",
@ -45,6 +47,11 @@ from .helpers import dumper_option, loader_option
"the nearest 1/beat_snap beat"
),
)
@loader_option(
"--merge",
is_flag=True,
help="For memon, if called on a folder, merge all the .memon files found",
)
def convert(
src: str,
dst: str,

View File

@ -0,0 +1,25 @@
{
"data": {
"ADV": {
"level": 5,
"notes": [
{
"l": 0,
"n": 1,
"p": 0,
"t": 1680
}
],
"resolution": 240
}
},
"metadata": {
"BPM": 180.28199768066406,
"album cover path": "2a03puritans.png",
"artist": "commandycan",
"music path": "Sky Bus For Hire.ogg",
"offset": -0.028,
"song title": "Sky Bus For Hire"
},
"version": "0.1.0"
}

View File

@ -0,0 +1,25 @@
{
"data": {
"BSC": {
"level": 1,
"notes": [
{
"l": 0,
"n": 2,
"p": 0,
"t": 1680
}
],
"resolution": 240
}
},
"metadata": {
"BPM": 180.28199768066406,
"album cover path": "2a03puritans.png",
"artist": "commandycan",
"music path": "Sky Bus For Hire.ogg",
"offset": -0.028,
"song title": "Sky Bus For Hire"
},
"version": "0.1.0"
}

View File

@ -0,0 +1,25 @@
{
"data": {
"EXT": {
"level": 10,
"notes": [
{
"l": 0,
"n": 15,
"p": 0,
"t": 1680
}
],
"resolution": 240
}
},
"metadata": {
"BPM": 180.28199768066406,
"album cover path": "2a03puritans.png",
"artist": "commandycan",
"music path": "Sky Bus For Hire.ogg",
"offset": -0.028,
"song title": "Sky Bus For Hire"
},
"version": "0.1.0"
}

View File

@ -0,0 +1,2 @@
"""This file is here so the test code can use importlib as a portable way to
open test data in this folder"""

View File

@ -1,15 +1,17 @@
from importlib import resources
from pathlib import Path
from click.testing import CliRunner
from jubeatools import song as jbt
from jubeatools.formats import LOADERS, Format
from ..cli import convert
from . import data
def test_that_ommiting_beat_snap_works() -> None:
"""
As pointed out by https://github.com/Stepland/jubeatools/issues/17
"""
"""As pointed out by https://github.com/Stepland/jubeatools/issues/17"""
runner = CliRunner()
with runner.isolated_filesystem(), resources.path(
data, "Life Without You.eve"
@ -20,3 +22,50 @@ def test_that_ommiting_beat_snap_works() -> None:
if result.exception:
raise result.exception
assert result.exit_code == 0
def test_that_is_flag_works_the_way_intended() -> None:
"""It's unclear to me what the default value is for an option with
is_flag=True"""
with resources.path(data, "Life Without You.eve") as p:
called_with_the_flag = convert.make_context(
"convert",
[str(p.resolve(strict=True)), "out.txt", "-f", "memo2", "--circlefree"],
)
assert called_with_the_flag.params["dumper_options"]["circle_free"] is True
called_without_the_flag = convert.make_context(
"convert", [str(p.resolve(strict=True)), "out.txt", "-f", "memo2"]
)
dumper_options = called_without_the_flag.params.get("dumper_options")
if dumper_options is not None:
circle_free = dumper_options.get("circle_free")
assert not circle_free
def test_that_the_merge_option_works_for_memon_files() -> None:
runner = CliRunner()
with runner.isolated_filesystem(), resources.path(data, "memon_merge") as p:
result = runner.invoke(
convert,
[
"--input-format",
"memon:v0.1.0",
str(p.resolve(strict=True)),
"--merge",
"out.memon",
"-f",
"memon:v0.1.0",
],
)
if result.exception:
raise result.exception
assert result.exit_code == 0
memon_loader = LOADERS[Format.MEMON_0_1_0]
bsc = memon_loader(p / "Sky Bus For Hire BSC.memon")
adv = memon_loader(p / "Sky Bus For Hire ADV.memon")
ext = memon_loader(p / "Sky Bus For Hire EXT.memon")
merged_by_cli = LOADERS[Format.MEMON_0_1_0](Path("out.memon"))
merged_with_python = jbt.Song.from_monochart_instances(bsc, adv, ext)
assert merged_by_cli == merged_with_python

View File

@ -1,5 +1,5 @@
"""
Module containing all the load/dump code for all file formats
"""
from .enum import Format
from .format_names import Format
from .loaders_and_dumpers import DUMPERS, LOADERS

View File

@ -1,16 +0,0 @@
from enum import Enum
class Format(str, Enum):
EVE = "eve"
JBSQ = "jbsq"
MALODY = "malody"
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"
MEMON_1_0_0 = "memon:v1.0.0"
MONO_COLUMN = "mono-column"
MEMO = "memo"
MEMO_1 = "memo1"
MEMO_2 = "memo2"

View File

@ -2,7 +2,7 @@ import json
import re
from pathlib import Path
from .enum import Format
from .format_names import Format
def guess_format(path: Path) -> Format:

View File

@ -349,4 +349,4 @@ def _load_memo_file(lines: List[str]) -> Song:
def load_memo(path: Path, **kwargs: Any) -> Song:
files = load_folder(path)
charts = [_load_memo_file(lines) for _, lines in files.items()]
return Song.from_monochart_instances(charts)
return Song.from_monochart_instances(*charts)

View File

@ -340,4 +340,4 @@ def _load_memo1_file(lines: List[str]) -> Song:
def load_memo1(path: Path, **kwargs: Any) -> Song:
files = load_folder(path)
charts = [_load_memo1_file(lines) for _, lines in files.items()]
return Song.from_monochart_instances(charts)
return Song.from_monochart_instances(*charts)

View File

@ -459,4 +459,4 @@ def _load_memo2_file(lines: List[str]) -> Song:
def load_memo2(path: Path, **kwargs: Any) -> Song:
files = load_folder(path)
charts = [_load_memo2_file(lines) for _, lines in files.items()]
return Song.from_monochart_instances(charts)
return Song.from_monochart_instances(*charts)

View File

@ -246,7 +246,7 @@ class MonoColumnParser(JubeatAnalyserParser):
def load_mono_column(path: Path, **kwargs: Any) -> Song:
files = load_folder(path)
charts = [_load_mono_column_file(lines) for _, lines in files.items()]
return Song.from_monochart_instances(charts)
return Song.from_monochart_instances(*charts)
def _load_mono_column_file(lines: List[str]) -> Song:

View File

@ -7,7 +7,7 @@ from hypothesis import note as hypothesis_note
from hypothesis import strategies as st
from jubeatools import song
from jubeatools.formats.enum import Format
from jubeatools.formats.format_names import Format
from jubeatools.formats.jubeat_analyser.memo.dump import _dump_memo_chart
from jubeatools.formats.jubeat_analyser.memo.load import MemoParser
from jubeatools.testutils import strategies as jbst

View File

@ -1,7 +1,7 @@
import math
from fractions import Fraction
from functools import singledispatch
from typing import List
from typing import List, Optional, Set
from more_itertools import numeric_range
@ -11,10 +11,12 @@ from jubeatools.formats.timemap import TimeMap
from .commons import AnyNote, Command, Event, bpm_to_value, ticks_at_beat
def make_events_from_chart(notes: List[AnyNote], timing: song.Timing) -> List[Event]:
def make_events_from_chart(
notes: List[AnyNote], timing: song.Timing, hakus: Optional[Set[song.BeatsTime]]
) -> List[Event]:
time_map = TimeMap.from_timing(timing)
note_events = make_note_events(notes, time_map)
timing_events = make_timing_events(notes, timing, time_map)
timing_events = make_timing_events(notes, timing, hakus, time_map)
return sorted(note_events + timing_events)
@ -38,14 +40,21 @@ def make_long_note_event(note: song.LongNote, time_map: TimeMap) -> Event:
def make_timing_events(
notes: List[AnyNote], timing: song.Timing, time_map: TimeMap
notes: List[AnyNote],
timing: song.Timing,
hakus: Optional[Set[song.BeatsTime]],
time_map: TimeMap,
) -> List[Event]:
bpm_events = [make_bpm_event(e, time_map) for e in timing.events]
end_beat = choose_end_beat(notes)
end_event = make_end_event(end_beat, time_map)
measure_events = make_measure_events(end_beat, time_map)
beat_events = make_beat_events(end_beat, time_map)
return bpm_events + measure_events + beat_events + [end_event]
if hakus is not None:
haku_events = dump_hakus(hakus, time_map)
else:
haku_events = make_regular_hakus(end_beat, time_map)
return bpm_events + measure_events + haku_events + [end_event]
def make_bpm_event(bpm_change: song.BPMEvent, time_map: TimeMap) -> Event:
@ -94,14 +103,18 @@ def make_measure_event(beat: song.BeatsTime, time_map: TimeMap) -> Event:
return Event(time=ticks, command=Command.MEASURE, value=0)
def make_beat_events(end_beat: song.BeatsTime, time_map: TimeMap) -> List[Event]:
def dump_hakus(hakus: Set[song.BeatsTime], time_map: TimeMap) -> List[Event]:
return [make_haku_event(beat, time_map) for beat in sorted(hakus)]
def make_regular_hakus(end_beat: song.BeatsTime, time_map: TimeMap) -> List[Event]:
start = song.BeatsTime(0)
stop = end_beat + song.BeatsTime(1, 2)
step = song.BeatsTime(1)
beats = numeric_range(start, stop, step)
return [make_beat_event(beat, time_map) for beat in beats]
return [make_haku_event(beat, time_map) for beat in beats]
def make_beat_event(beat: song.BeatsTime, time_map: TimeMap) -> Event:
def make_haku_event(beat: song.BeatsTime, time_map: TimeMap) -> Event:
ticks = ticks_at_beat(beat, time_map)
return Event(time=ticks, command=Command.HAKU, value=0)

View File

@ -10,8 +10,8 @@ from ..dump_tools import make_events_from_chart
def _dump_eve(song: song.Song, **kwargs: dict) -> List[ChartFile]:
res = []
for dif, chart, timing in song.iter_charts_with_applicable_timing():
events = make_events_from_chart(chart.notes, timing)
for dif, chart, timing, hakus in song.iter_charts():
events = make_events_from_chart(chart.notes, timing, hakus)
chart_text = "\n".join(e.dump() for e in events)
chart_bytes = chart_text.encode("ascii")
res.append(ChartFile(chart_bytes, song, dif, chart))

View File

@ -11,7 +11,7 @@ from ..load_tools import make_chart_from_events
def load_eve(path: Path, *, beat_snap: int = 240, **kwargs: Any) -> song.Song:
files = load_folder(path)
charts = [_load_eve(l, p, beat_snap=beat_snap) for p, l in files.items()]
return song.Song.from_monochart_instances(charts)
return song.Song.from_monochart_instances(*charts)
def load_file(path: Path) -> List[str]:

View File

@ -15,8 +15,8 @@ from . import construct
def _dump_jbsq(song: song.Song, **kwargs: dict) -> List[ChartFile]:
res = []
for dif, chart, timing in song.iter_charts_with_applicable_timing():
events = make_events_from_chart(chart.notes, timing)
for dif, chart, timing, hakus in song.iter_charts():
events = make_events_from_chart(chart.notes, timing, hakus)
jbsq_chart = make_jbsq_chart(events, chart.notes)
chart_bytes = construct.jbsq.build(jbsq_chart)
res.append(ChartFile(chart_bytes, song, dif, chart))

View File

@ -15,7 +15,7 @@ def load_jbsq(path: Path, *, beat_snap: int = 240, **kwargs: Any) -> song.Song:
load_jbsq_file(bytes_, path, beat_snap=beat_snap)
for path, bytes_ in files.items()
]
return song.Song.from_monochart_instances(charts)
return song.Song.from_monochart_instances(*charts)
def load_file(path: Path) -> bytes:

View File

@ -1,5 +1,7 @@
from decimal import Decimal
from typing import Iterable, List
from typing import Iterable, List, Optional, Set
from more_itertools import numeric_range
from jubeatools import song
from jubeatools.formats.load_tools import round_beats
@ -36,17 +38,22 @@ def make_chart_from_events(events: Iterable[Event], beat_snap: int = 240) -> son
]
all_notes = sorted(tap_notes + long_notes, key=lambda n: (n.time, n.position))
timing = time_map.convert_to_timing_info(beat_snap=beat_snap)
return song.Chart(level=Decimal(0), timing=timing, notes=all_notes)
end_tick = events_by_command[Command.END].pop().time
hakus = make_hakus(
[e.time for e in events_by_command[Command.HAKU]],
end_tick,
time_map,
beat_snap,
)
return song.Chart(level=Decimal(0), timing=timing, notes=all_notes, hakus=hakus)
def make_tap_note(
ticks: int, value: int, time_map: TimeMap, beat_snap: int
) -> song.TapNote:
seconds = ticks_to_seconds(ticks)
raw_beats = time_map.beats_at(seconds)
beats = round_beats(raw_beats, beat_snap)
time = beats_at_tick(ticks, time_map, beat_snap)
position = song.NotePosition.from_index(value)
return song.TapNote(time=beats, position=position)
return song.TapNote(time=time, position=position)
def make_long_note(
@ -67,3 +74,52 @@ def make_long_note(
return song.LongNote(
time=beats, position=position, duration=beats_duration, tail_tip=tail_pos
)
def make_hakus(
hakus: List[int], end: int, time_map: TimeMap, beat_snap: int
) -> Optional[Set[song.BeatsTime]]:
"""Try to detect if the haku pattern is regular, in which case return None,
otherwise return the parsed hakus"""
roughly_rounded_hakus = make_raw_hakus(hakus, time_map, beat_snap=4)
rough_end = beats_at_tick(end, time_map, beat_snap=4)
if follows_regular_haku_pattern(roughly_rounded_hakus, rough_end):
return None
else:
return make_raw_hakus(hakus, time_map, beat_snap)
def make_raw_hakus(
hakus: List[int], time_map: TimeMap, beat_snap: int
) -> Set[song.BeatsTime]:
return set(beats_at_tick(haku, time_map, beat_snap) for haku in hakus)
def follows_regular_haku_pattern(
hakus: Set[song.BeatsTime], end_command: song.BeatsTime
) -> bool:
"""Regular hakus extend at least till the END command in a 4/4 rhythm"""
if len(hakus) == 0:
return False
start = min(hakus)
if (start % 1) != 0:
return False
haku_end = max(hakus)
if (haku_end % 1) != 0:
return False
if haku_end < end_command:
return False
stop = haku_end + song.BeatsTime(1, 2)
step = song.BeatsTime(1)
regular = numeric_range(start, stop, step)
return sorted(hakus) == list(regular)
def beats_at_tick(tick: int, time_map: TimeMap, beat_snap: int) -> song.BeatsTime:
seconds = ticks_to_seconds(tick)
raw_beats = time_map.beats_at(seconds)
return round_beats(raw_beats, beat_snap)

View File

@ -12,7 +12,7 @@ simple_beat_strat = jbst.beat_time(
@st.composite
def eve_compatible_song(draw: st.DrawFn) -> song.Song:
"""eve only keeps notes, timing info and difficulty,
"""eve only keeps notes, hakus, timing info and difficulty,
the precision you can get out of it is also severly limited"""
diff = draw(st.sampled_from(list(song.Difficulty)))
chart = draw(
@ -42,6 +42,7 @@ def eve_compatible_song(draw: st.DrawFn) -> song.Song:
beat_time_strat=simple_beat_strat,
),
level_strat=st.just(Decimal(0)),
hakus_strat=st.one_of(st.none(), st.sets(simple_beat_strat)),
)
)
return song.Song(

View File

@ -1,7 +1,7 @@
from typing import Dict
from . import jubeat_analyser, konami, malody, memon
from .enum import Format
from .format_names import Format
from .typing import Dumper, Loader
LOADERS: Dict[Format, Loader] = {

View File

@ -18,7 +18,7 @@ from . import schema as malody
def load_malody(path: Path, **kwargs: Any) -> song.Song:
files = load_folder(path)
charts = [load_malody_file(d) for d in files.values()]
return song.Song.from_monochart_instances(charts)
return song.Song.from_monochart_instances(*charts)
def load_file(path: Path) -> Any:

View File

@ -31,7 +31,7 @@ def make_memon_folder_loader(memon_loader: Callable[[Any], jbt.Song]) -> Loader:
)
charts = [memon_loader(d) for d in files.values()]
return jbt.Song.from_monochart_instances(charts)
return jbt.Song.from_monochart_instances(*charts)
return load

View File

@ -1,7 +1,5 @@
from pathlib import Path
from typing import Any, Union
from multidict import MultiDict
from typing import Any, Dict, Union
from jubeatools import song as jbt
from jubeatools.utils import none_or
@ -38,18 +36,17 @@ def _load_memon_legacy(raw_memon: Any) -> jbt.Song:
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=file["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-file["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
charts: Dict[str, jbt.Chart] = {}
for memon_chart in file["data"]:
charts.add(
memon_chart["dif_name"],
jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
),
difficulty = memon_chart["dif_name"]
chart = jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
)
charts[difficulty] = chart
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
@ -70,18 +67,16 @@ def _load_memon_0_1_0(raw_memon: Any) -> jbt.Song:
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=file["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-file["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
charts: Dict[str, jbt.Chart] = {}
for difficulty, memon_chart in file["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"]
],
),
chart = jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
)
charts[difficulty] = chart
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
@ -109,18 +104,16 @@ def _load_memon_0_2_0(raw_memon: Any) -> jbt.Song:
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=file["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-file["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
charts: Dict[str, jbt.Chart] = {}
for difficulty, memon_chart in file["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"]
],
),
chart = jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
)
charts[difficulty] = chart
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)
@ -149,18 +142,16 @@ def _load_memon_0_3_0(raw_memon: Any) -> jbt.Song:
events=[jbt.BPMEvent(time=jbt.BeatsTime(0), BPM=file["metadata"]["BPM"])],
beat_zero_offset=jbt.SecondsTime(-file["metadata"]["offset"]),
)
charts: MultiDict[jbt.Chart] = MultiDict()
charts: Dict[str, jbt.Chart] = {}
for difficulty, memon_chart in file["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"]
],
),
chart = jbt.Chart(
level=memon_chart["level"],
notes=[
_load_memon_note_v0(note, memon_chart["resolution"])
for note in memon_chart["notes"]
],
)
charts[difficulty] = chart
return jbt.Song(metadata=metadata, charts=charts, common_timing=common_timing)

View File

@ -4,7 +4,7 @@ import hypothesis.strategies as st
from hypothesis import given
from jubeatools import song
from jubeatools.formats.enum import Format
from jubeatools.formats.format_names import Format
from jubeatools.testutils import strategies as jbst
from jubeatools.testutils.test_patterns import dump_and_load_then_compare

View File

@ -12,9 +12,7 @@ from ..tools import make_memon_dumper
from . import schema as memon
def _dump_memon_1_0_0(
song: jbt.Song, use_fractions: bool = False, **kwargs: Any
) -> SongFile:
def _dump_memon_1_0_0(song: jbt.Song, **kwargs: Any) -> SongFile:
metadata = dump_metadata(song.metadata)
common_timing = dump_file_timing(song)
charts = {

View File

@ -5,7 +5,7 @@ import hypothesis.strategies as st
from hypothesis import given
from jubeatools import song
from jubeatools.formats.enum import Format
from jubeatools.formats.format_names import Format
from jubeatools.testutils import strategies as jbst
from jubeatools.testutils.test_patterns import dump_and_load_then_compare

View File

@ -17,6 +17,7 @@ from pathlib import Path
from typing import (
Any,
Callable,
Dict,
Iterable,
Iterator,
List,
@ -28,8 +29,6 @@ from typing import (
Union,
)
from multidict import MultiDict
from jubeatools.utils import none_or
BeatsTime = Fraction
@ -223,14 +222,13 @@ class Metadata:
preview_file: Optional[Path] = None
@classmethod
def permissive_merge(cls, metadatas: Iterable["Metadata"]) -> "Metadata":
def permissive_merge(cls, *metadatas: "Metadata") -> "Metadata":
"""Make the "sum" of all the given metadata instances, if possible. If
several instances have different defined values for the same field,
merging will fail. Fields with Noneor empty values (empty string or
empty path) are conscidered undefined and their values can be replaced
by an actual value if supplied by at least one object from the given
iterable."""
metadatas = list(metadatas)
return cls(
**{f.name: _get_common_value(f, metadatas) for f in fields(cls)},
)
@ -280,18 +278,18 @@ class Song:
A Song is a set of charts with associated metadata"""
metadata: Metadata
charts: Mapping[str, Chart] = field(default_factory=MultiDict)
charts: Mapping[str, Chart] = field(default_factory=dict)
common_timing: Optional[Timing] = None
common_hakus: Optional[Set[BeatsTime]] = None
@classmethod
def from_monochart_instances(cls, songs: Iterable["Song"]) -> "Song":
metadata = Metadata.permissive_merge(song.metadata for song in songs)
charts: MultiDict[Chart] = MultiDict()
def from_monochart_instances(cls, *songs: "Song") -> "Song":
metadata = Metadata.permissive_merge(*(song.metadata for song in songs))
charts: Dict[str, Chart] = {}
for song in songs:
song.remove_common_timing()
song.remove_common_hakus()
charts.extend(song.charts)
charts.update(song.charts)
merged = cls(
metadata=metadata,
@ -351,3 +349,15 @@ class Song:
f"Neither song nor {dif} chart have any timing information"
)
yield dif, chart, timing
def iter_charts(
self,
) -> Iterator[Tuple[str, Chart, Timing, Optional[Set[BeatsTime]]]]:
for dif, chart in self.charts.items():
timing = chart.timing or self.common_timing
if timing is None:
raise ValueError(
f"Neither song nor {dif} chart have any timing information"
)
hakus = chart.hakus if chart.hakus is not None else self.common_hakus
yield dif, chart, timing, hakus

View File

@ -10,7 +10,6 @@ from pathlib import Path
from typing import Dict, Iterable, Optional, Set, Union
import hypothesis.strategies as st
from multidict import MultiDict
from jubeatools.song import (
BeatsTime,
@ -286,14 +285,9 @@ def song(
chart_strat: st.SearchStrategy[Chart] = chart(),
metadata_strat: st.SearchStrategy[Metadata] = metadata(),
) -> Song:
diffs = draw(diffs_strat)
charts: MultiDict[Chart] = MultiDict()
for diff_name in diffs:
charts.add(diff_name, draw(chart_strat))
return Song(
metadata=draw(metadata_strat),
charts=charts,
charts={difficulty: draw(chart_strat) for difficulty in draw(diffs_strat)},
common_timing=draw(common_timing_strat),
common_hakus=draw(common_hakus_strat),
)

View File

@ -6,8 +6,7 @@ from typing import Callable, ContextManager, Iterator, Optional
from hypothesis import note
from jubeatools import song
from jubeatools.formats import DUMPERS, LOADERS
from jubeatools.formats.enum import Format
from jubeatools.formats import DUMPERS, LOADERS, Format
from jubeatools.formats.guess import guess_format

84
poetry.lock generated
View File

@ -238,14 +238,6 @@ category = "main"
optional = false
python-versions = ">=3.5"
[[package]]
name = "multidict"
version = "5.2.0"
description = "multidict implementation"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mypy"
version = "0.910"
@ -523,7 +515,7 @@ typing-extensions = ">=3.7.4"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "41155bca4070edc9eb6e6369890af55397251caeab4e5c2db62ef7785410853e"
content-hash = "971f32ab1478240f072615b119a1df87aebccfb26e6ee4cf013d938ff15db690"
[metadata.files]
atomicwrites = [
@ -596,80 +588,6 @@ more-itertools = [
{file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"},
{file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"},
]
multidict = [
{file = "multidict-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55"},
{file = "multidict-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e"},
{file = "multidict-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7"},
{file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf"},
{file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b"},
{file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5"},
{file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f"},
{file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747"},
{file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52"},
{file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628"},
{file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda"},
{file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a"},
{file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86"},
{file = "multidict-5.2.0-cp310-cp310-win32.whl", hash = "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7"},
{file = "multidict-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f"},
{file = "multidict-5.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d"},
{file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37"},
{file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b"},
{file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1"},
{file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4"},
{file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02"},
{file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5"},
{file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858"},
{file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677"},
{file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded"},
{file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d"},
{file = "multidict-5.2.0-cp36-cp36m-win32.whl", hash = "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9"},
{file = "multidict-5.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0"},
{file = "multidict-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0"},
{file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11"},
{file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422"},
{file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3"},
{file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d"},
{file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac"},
{file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0"},
{file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704"},
{file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23"},
{file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d"},
{file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b"},
{file = "multidict-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef"},
{file = "multidict-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a"},
{file = "multidict-5.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8"},
{file = "multidict-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6"},
{file = "multidict-5.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065"},
{file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e"},
{file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c"},
{file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64"},
{file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031"},
{file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d"},
{file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01"},
{file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4"},
{file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b"},
{file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac"},
{file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22"},
{file = "multidict-5.2.0-cp38-cp38-win32.whl", hash = "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940"},
{file = "multidict-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0"},
{file = "multidict-5.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24"},
{file = "multidict-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21"},
{file = "multidict-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae"},
{file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6"},
{file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c"},
{file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0"},
{file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17"},
{file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0"},
{file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1"},
{file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621"},
{file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341"},
{file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b"},
{file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5"},
{file = "multidict-5.2.0-cp39-cp39-win32.whl", hash = "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8"},
{file = "multidict-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac"},
{file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"},
]
mypy = [
{file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"},
{file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"},

View File

@ -8,7 +8,6 @@ repository = "https://github.com/Stepland/jubeatools"
[tool.poetry.dependencies]
python = "^3.8"
multidict = "^5.1.0"
click = "^8.0.3"
path = "^15.1.2"
simplejson = "^3.17.0"