Skip to content
Merged

Dev #37

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

## Added
- `BaseRemData` class from which the corresponding classes for albums and tracks are inherited.
- Full parsing of track replaygain fields.

### Changed
- Type of album rem data: `AlbumRemData` instead `RemData`.
- Field names of the album replaygain on: `replaygain_gain` and `replaygain_peak` instead `replaygain_album_gain` and `replaygain_album_peak`.

### Fixed
- Parsing of title case fields with the *strict_title_case* flag; now, when an error occurs, an `CueValidationError` is thrown instead of an `ValueError`.
- `CHANGELOG.md` markup.
Expand Down
46 changes: 28 additions & 18 deletions cuetools/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@
from cuetools.types.title_case import TitleCase


class BaseRemData(BaseModel):
model_config = ConfigDict(validate_assignment=True)

replaygain_gain: ReplayGainGain | None = Field(
default=None, description='Album replay gain, value format [-]a.bb dB'
)
replaygain_peak: ReplayGainPeak | None = Field(
default=None, description='Album peak, value format c.dddddd'
)


class AlbumRemData(BaseRemData):
genre: str | None = Field(default=None, description='Album genre')
date: int | None = Field(default=None, description='Album release date')

def set_genre(self, genre: TitleCase) -> None:
"""Set album genre with a Title Case validation using `TitleCase` class consructor for string"""
self.genre = genre


class TrackRemData(BaseRemData): ...


class TrackData(BaseModel):
"""Represents a single track within a CUE sheet.

Expand All @@ -22,6 +45,9 @@ class TrackData(BaseModel):
)
title: str | None = Field(default=None, description='Track title')
performer: str | None = Field(default=None, description='Track performer')
rem: TrackRemData = Field(
default_factory=TrackRemData, description='Track additional rem meta'
)
index00: FrameTime | None = Field(
default=None,
description="The index 00 (the end of the prev track), corresponds to the line like *'INDEX 00 00:00:00'*",
Expand All @@ -44,22 +70,6 @@ def set_title(self, title: TitleCase) -> None:
self.title = title


class RemData(BaseModel):
model_config = ConfigDict(validate_assignment=True)
genre: str | None = Field(default=None, description='Album genre')
date: int | None = Field(default=None, description='Album release date')
replaygain_album_gain: ReplayGainGain | None = Field(
default=None, description='Album replay gain, value format [-]a.bb dB'
)
replaygain_album_peak: ReplayGainPeak | None = Field(
default=None, description='Album peak, value format c.dddddd'
)

def set_genre(self, genre: TitleCase) -> None:
"""Set album genre with a Title Case validation using `TitleCase` class consructor for string"""
self.genre = genre


class AlbumData(BaseModel):
"""Represents a parsed CUE sheet at the album level.

Expand All @@ -69,8 +79,8 @@ class AlbumData(BaseModel):
model_config = ConfigDict(validate_assignment=True)
performer: str | None = Field(default=None, description='Album performer')
title: str | None = Field(default=None, description='Album title')
rem: RemData = Field(
default_factory=RemData, description='Album additional rem meta'
rem: AlbumRemData = Field(
default_factory=AlbumRemData, description='Album additional rem meta'
)
tracks: list[TrackData] = Field(
default_factory=list[TrackData], description='Track list of this album'
Expand Down
38 changes: 32 additions & 6 deletions cuetools/parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,44 @@ def load_f_iter(cue: Iterator[str], strict_title_case: bool = False) -> AlbumDat
)
case Token.REPLAYGAIN_ALBUM_GAIN:
try:
album.rem.replaygain_album_gain = value.lexeme # type: ignore[assignment]
album.rem.replaygain_gain = value.lexeme # type: ignore[assignment]
except ValueError as e:
raise CueValidationError(
current_line, line, value.pos, value.lexeme, e
)
case Token.REPLAYGAIN_ALBUM_PEAK:
try:
album.rem.replaygain_album_peak = value.lexeme # type: ignore[assignment]
album.rem.replaygain_peak = value.lexeme # type: ignore[assignment]
except ValueError as e:
raise CueValidationError(
current_line, line, value.pos, value.lexeme, e
)
case Token.REPLAYGAIN_TRACK_GAIN:
if not current_track:
raise CueParseError(
current_line,
line,
'any TRACK tag before',
tokens[0].lexeme,
tokens[1].pos,
)
try:
current_track.rem.replaygain_gain = value.lexeme # type: ignore[assignment]
except ValueError as e:
raise CueValidationError(
current_line, line, value.pos, value.lexeme, e
)
case Token.REPLAYGAIN_TRACK_PEAK:
if not current_track:
raise CueParseError(
current_line,
line,
'any TRACK tag before',
tokens[0].lexeme,
tokens[1].pos,
)
try:
current_track.rem.replaygain_peak = value.lexeme # type: ignore[assignment]
except ValueError as e:
raise CueValidationError(
current_line, line, value.pos, value.lexeme, e
Expand All @@ -146,10 +176,6 @@ def load_f_iter(cue: Iterator[str], strict_title_case: bool = False) -> AlbumDat
...
case Token.COMMENT:
...
case Token.REPLAYGAIN_TRACK_GAIN:
...
case Token.REPLAYGAIN_TRACK_PEAK:
...
case _:
raise CueParseError(
current_line,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cuetools"
version = "1.0.3"
version = "1.1.0"
description = "Lightweight CUE sheet toolkit for Python"
authors = [
{name = "Olezhich"}
Expand Down
66 changes: 49 additions & 17 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@

import pytest
from cuetools import AlbumData, TrackData
from cuetools.models import RemData
from cuetools.models import AlbumRemData, TrackRemData

TAB = 2


def track_gen(
filename: str, songname: str, tracks_count: int = 1, tab: int = TAB
filename: str,
songname: str,
gain: float,
peak: float,
tracks_count: int = 1,
tab: int = TAB,
) -> str:
out = f'''FILE "{filename}" {'WAVE' if filename.endswith('.flac') else ''}\n'''
filename = filename.split('.')[0]
Expand All @@ -23,6 +28,8 @@ def track_gen(
s = '0' + s if len(s) < 2 else s
out += f'''{' ' * tab}TRACK {s} AUDIO
{' ' * 2 * tab}TITLE "{songname}"
{' ' * 2 * tab}REM REPLAYGAIN_TRACK_GAIN {gain:.2f} dB
{' ' * 2 * tab}REM REPLAYGAIN_TRACK_PEAK {peak:.6f}
{' ' * 2 * tab}INDEX 01 00:00:00\n'''
suf += 1
if out.endswith('\n'):
Expand Down Expand Up @@ -88,12 +95,16 @@ def album_rem_strict(request: Any) -> list[str]:

@pytest.fixture()
def album_tracks_default() -> list[str]:
return [track_gen(f'0{i} - Song 0{i}.flac', f'song {i}') for i in range(1, 8)]
return [
track_gen(f'0{i} - Song 0{i}.flac', f'song {i}', 10.0, 0.9) for i in range(1, 8)
]


@pytest.fixture()
def album_tracks_strict() -> list[str]:
return [track_gen(f'0{i} - Song 0{i}.flac', f'Song {i}') for i in range(1, 8)]
return [
track_gen(f'0{i} - Song 0{i}.flac', f'Song {i}', 10.0, 0.9) for i in range(1, 8)
]


# Cues
Expand Down Expand Up @@ -129,7 +140,7 @@ def cue_sample_one_file_many_tracks_default(
album_meta_default: list[str], album_rem_default: list[str]
) -> str:
cuesheet = album_meta_default + album_rem_default
cuesheet += [track_gen('The Title Of Album.flac', 'track title', 7)]
cuesheet += [track_gen('The Title Of Album.flac', 'track title', 10.0, 0.9, 7)]
return '\n'.join(cuesheet)


Expand All @@ -138,7 +149,7 @@ def cue_sample_one_file_many_tracks_strict(
album_meta_strict: list[str], album_rem_strict: list[str]
) -> str:
cuesheet = album_meta_strict + album_rem_strict
cuesheet += [track_gen('The Title Of Album.flac', 'Track Title', 7)]
cuesheet += [track_gen('The Title Of Album.flac', 'Track Title', 10.0, 0.9, 7)]
return '\n'.join(cuesheet)


Expand All @@ -152,11 +163,16 @@ def obj_sample_one_file_one_track_default() -> AlbumData:
album = AlbumData(
performer='the performer',
title='the title of album',
rem=RemData(genre='rock', date=1969),
rem=AlbumRemData(genre='rock', date=1969),
)
for i in range(1, 8):
album.add_track(
TrackData(track=i, title=f'song {i}', file=Path(f'0{i} - Song 0{i}.flac'))
TrackData(
track=i,
title=f'song {i}',
file=Path(f'0{i} - Song 0{i}.flac'),
rem=TrackRemData(replaygain_gain=10.0, replaygain_peak=0.9),
)
)
return album

Expand All @@ -166,11 +182,16 @@ def obj_sample_one_file_one_track_strict() -> AlbumData:
album = AlbumData(
performer='The Performer',
title='The Title Of Album',
rem=RemData(genre='Rock', date=1969),
rem=AlbumRemData(genre='Rock', date=1969),
)
for i in range(1, 8):
album.add_track(
TrackData(track=i, title=f'Song {i}', file=Path(f'0{i} - Song 0{i}.flac'))
TrackData(
track=i,
title=f'Song {i}',
file=Path(f'0{i} - Song 0{i}.flac'),
rem=TrackRemData(replaygain_gain=10.0, replaygain_peak=0.9),
)
)
return album

Expand All @@ -183,12 +204,15 @@ def obj_sample_one_file_many_tracks_default() -> AlbumData:
album = AlbumData(
performer='the performer',
title='the title of album',
rem=RemData(genre='rock', date=1969),
rem=AlbumRemData(genre='rock', date=1969),
)
for i in range(1, 8):
album.add_track(
TrackData(
track=i, title='track title', file=Path('The Title Of Album.flac')
track=i,
title='track title',
file=Path('The Title Of Album.flac'),
rem=TrackRemData(replaygain_gain=10.0, replaygain_peak=0.9),
)
)
return album
Expand All @@ -199,12 +223,15 @@ def obj_sample_one_file_many_tracks_strict() -> AlbumData:
album = AlbumData(
performer='The Performer',
title='The Title Of Album',
rem=RemData(genre='Rock', date=1969),
rem=AlbumRemData(genre='Rock', date=1969),
)
for i in range(1, 8):
album.add_track(
TrackData(
track=i, title='Track Title', file=Path('The Title Of Album.flac')
track=i,
title='Track Title',
file=Path('The Title Of Album.flac'),
rem=TrackRemData(replaygain_gain=10.0, replaygain_peak=0.9),
)
)
return album
Expand All @@ -213,7 +240,7 @@ def obj_sample_one_file_many_tracks_strict() -> AlbumData:
@pytest.fixture()
def cue_sample_one_file_many_tracks() -> str:
cuesheet = album_rem_default() + album_meta_default()
cuesheet += [track_gen('The Title Of Album.flac', 'song_name', 7)]
cuesheet += [track_gen('The Title Of Album.flac', 'song_name', 10.0, 0.9, 7)]
return '\n'.join(cuesheet)


Expand All @@ -222,11 +249,16 @@ def obj_sample_one_file_many_tracks() -> AlbumData:
album = AlbumData(
performer='The Performer',
title='The Title Of Album',
rem=RemData(genre='Rock', date=1969),
rem=AlbumRemData(genre='Rock', date=1969),
)
for i in range(1, 8):
album.add_track(
TrackData(track=i, title='song_name', file=Path('The Title Of Album.flac'))
TrackData(
track=i,
title='song_name',
file=Path('The Title Of Album.flac'),
rem=TrackRemData(replaygain_gain=10.0, replaygain_peak=0.9),
)
)
return album

Expand Down
6 changes: 4 additions & 2 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import logging

from cuetools.models import AlbumData, RemData
from cuetools.models import AlbumData, AlbumRemData
from cuetools.parser.errors import CueParseError, CueValidationError

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -61,6 +61,8 @@ def test_real_cue(self, cue_sample_real: str):
assert res.performer == 'Scorpions', (
'Thats correct if it can parse the entire file without throwing an errors'
)
assert res.tracks[3].rem.replaygain_gain == -6.73
assert res.tracks[3].rem.replaygain_peak == 1.035224


def test_line_parsing():
Expand Down Expand Up @@ -114,7 +116,7 @@ def test_line_parsing():
cue = cuetools.loads(cue_sheet)
logger.debug(cue)

assert cue == AlbumData(rem=RemData(genre='Blues', date=1969))
assert cue == AlbumData(rem=AlbumRemData(genre='Blues', date=1969))

cue_sheet = """REM REPLAYGAIN_ALBUM_GAIN 12.5"""
with pytest.raises(cuetools.CueValidationError) as e:
Expand Down
Loading