Skip to content
Merged
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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

## [1.0.1] - 2025-11-30

### Added
- Support for some new REM keywords in parser.

### Fixed
- Fixed metadata and URLs in `pyproject.toml`.
- Metadata and URLs in `pyproject.toml`.
- Improved formatting and clarity in `CHANGELOG.md`, added older versions.
- Track index validation. Now, when INDEX 00 is greater than 0, everything works correctly.

## [1.0.0] - 2025-11-27

Expand Down Expand Up @@ -82,6 +88,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).


[Unreleased]: https://github.com/Olezhich/CueTools/compare/v1.0.0...main
[1.0.1]: https://github.com/Olezhich/CueTools/compare/v1.0.1...v1.0.1
[1.0.0]: https://github.com/Olezhich/CueTools/compare/v0.1.5...v1.0.0
[0.1.5]: https://github.com/Olezhich/CueTools/compare/v0.1.4...v0.1.5
[0.1.4]: https://github.com/Olezhich/CueTools/compare/v0.1.3...v0.1.4
Expand Down
7 changes: 3 additions & 4 deletions cuetools/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations
from pathlib import Path
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field

from cuetools.types import FrameTime, ReplayGainGain, ReplayGainPeak
from cuetools.types.frame_time import FrameTimeCls
Expand Down Expand Up @@ -32,11 +32,9 @@ class TrackData(BaseModel):
description="The index 01 (the beginning of the current track), corresponds to the line like *'INDEX 01 00:00:00'*",
)

@model_validator(mode='after')
def validate_index(self) -> TrackData:
def validate_index(self) -> None:
if self.index00 is not None and self.index00.frames > self.index01.frames:
raise ValueError('Expected INDEX 00 <= INDEX 01')
return self

def set_performer(self, performer: TitleCase) -> None:
"""Set track performer with a **Title Case** validation using `TitleCase` class consructor for string"""
Expand Down Expand Up @@ -80,6 +78,7 @@ class AlbumData(BaseModel):
)

def add_track(self, track: TrackData) -> None:
track.validate_index()
self.tracks.append(track)

def set_performer(self, performer: TitleCase) -> None:
Expand Down
6 changes: 6 additions & 0 deletions cuetools/parser/lex.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ class Token(Enum):
REM = r'(REM)\b'
GENRE = r'(GENRE)\b'
DATE = r'(DATE)\b'

REPLAYGAIN_ALBUM_GAIN = r'(REPLAYGAIN_ALBUM_GAIN)\b'
REPLAYGAIN_ALBUM_PEAK = r'(REPLAYGAIN_ALBUM_PEAK)\b'
REPLAYGAIN_TRACK_GAIN = r'(REPLAYGAIN_TRACK_GAIN)\b'
REPLAYGAIN_TRACK_PEAK = r'(REPLAYGAIN_TRACK_PEAK)\b'

DISCID = r'(DISCID)\b'
COMMENT = r'(COMMENT)\b'

TRACK = r'TRACK (\d\d+)\b'
AUDIO = r'(AUDIO)\b'
Expand Down
16 changes: 15 additions & 1 deletion cuetools/parser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ def load_f_iter(cue: Iterator[str], strict_title_case: bool = False) -> AlbumDat
raise CueValidationError(
current_line, line, value.pos, value.lexeme, e
)
case Token.DISCID:
...
case Token.COMMENT:
...
case Token.REPLAYGAIN_TRACK_GAIN:
...
case Token.REPLAYGAIN_TRACK_PEAK:
...
case _:
raise CueParseError(
current_line,
Expand All @@ -168,7 +176,12 @@ def load_f_iter(cue: Iterator[str], strict_title_case: bool = False) -> AlbumDat
tokens[0].pos,
)
if current_track:
album.add_track(current_track)
try:
album.add_track(current_track)
except ValueError as e:
raise CueValidationError(
current_line, line, 0, 'Invalid previous track', e
)
current_track = TrackData(
track=int(tokens[0].lexeme),
file=current_file[0],
Expand All @@ -195,6 +208,7 @@ def load_f_iter(cue: Iterator[str], strict_title_case: bool = False) -> AlbumDat
current_track.index00 = tokens[1].lexeme # type: ignore[assignment]
elif index_type == 1:
current_track.index01 = tokens[1].lexeme # type: ignore[assignment]
print('INDEX WARN:: ', tokens[1].lexeme, current_track.index01)
case _:
raise CueParseError(
current_line,
Expand Down
119 changes: 51 additions & 68 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,71 +232,54 @@ def obj_sample_one_file_many_tracks() -> AlbumData:


@pytest.fixture()
def cue_sample_rem():
album = album_rem_gen(
genre='Hard Rock',
date='1969',
replaygain_album_gain='-4.10 db',
replaygain_album_peak='0.987654',
)
return '\n'.join(album)


@pytest.fixture()
def obj_sample_rem() -> AlbumData:
album = AlbumData(
rem=RemData(
genre='Hard Rock',
date=1969,
replaygain_album_gain='-4.10 db', # type: ignore
replaygain_album_peak='0.987654', # type: ignore
)
)
return album


if __name__ == '__main__':
print('___album rem tests___')

print(album_rem_gen(genre='Rock', date='1969', comment='comment'))
print(
album_rem_gen(
genre=['Hard Rock', 'Progressive Rock', 'Rock'],
date='1969',
comment='comment',
)
)
print(album_rem_gen(quotes=True, genre='Rock', date='1969', comment='comment'))
print(
album_rem_gen(
quotes=True,
genre=['Hard Rock', 'Progressive Rock', 'Rock'],
date='1969',
comment='comment',
)
)

print('___album meta tests___')

print(album_meta_gen(performer='The Performer', title='The Title Of Album'))
print(
album_meta_gen(
quotes=True, performer='The Performer', title='The Title Of Album'
)
)

print('___another tests___')

print(track_gen('song.flac', 'song', 3))
print(track_gen('song 05.flac', 'song', 1))
print([track_gen(f'0{i} - Song 0{i}.flac', f'Song {i}') for i in range(1, 8)])

print('___cue_gen_test___')

cuesheet = album_rem_default() + album_meta_default()
cuesheet += [track_gen(f'0{i} - Song 0{i}.flac', f'Song {i}') for i in range(1, 8)]
# for i in cuesheet:
# print(i.endswith('\n'), i)

# cuesheet = [i[-1:] if i.endswith('\n') else i for i in cuesheet]
print('\n'.join(cuesheet))
def cue_sample_real() -> str:
return """REM GENRE Hard Rock
REM DATE 1972
REM DISCID 12345678
REM COMMENT ExactAudioCopy v1.0b3
PERFORMER "Scorpions"
TITLE "Lonesome Crow"
REM REPLAYGAIN_ALBUM_GAIN -7.99 dB
REM REPLAYGAIN_ALBUM_PEAK 1.054599
FILE "Scorpions - Lonesome Crow.flac" WAVE
TRACK 01 AUDIO
TITLE "I'm Going Mad"
REM REPLAYGAIN_TRACK_GAIN -7.97 dB
REM REPLAYGAIN_TRACK_PEAK 1.033902
INDEX 01 00:00:00
TRACK 02 AUDIO
TITLE "It All Depends"
REM REPLAYGAIN_TRACK_GAIN -8.17 dB
REM REPLAYGAIN_TRACK_PEAK 1.054599
INDEX 01 04:53:27
TRACK 03 AUDIO
TITLE "Leave Me"
REM REPLAYGAIN_TRACK_GAIN -8.56 dB
REM REPLAYGAIN_TRACK_PEAK 1.053317
INDEX 00 08:17:15
INDEX 01 08:18:42
TRACK 04 AUDIO
TITLE "In Search Of The Peace Of Mind"
REM REPLAYGAIN_TRACK_GAIN -6.73 dB
REM REPLAYGAIN_TRACK_PEAK 1.035224
INDEX 00 13:21:00
INDEX 01 13:22:52
TRACK 05 AUDIO
TITLE "Inheritance"
REM REPLAYGAIN_TRACK_GAIN -8.17 dB
REM REPLAYGAIN_TRACK_PEAK 1.038148
INDEX 00 18:16:10
INDEX 01 18:17:62
TRACK 06 AUDIO
TITLE "Action"
REM REPLAYGAIN_TRACK_GAIN -8.43 dB
REM REPLAYGAIN_TRACK_PEAK 1.031126
INDEX 00 22:55:50
INDEX 01 22:58:17
TRACK 07 AUDIO
TITLE "Lonesome Crow"
REM REPLAYGAIN_TRACK_GAIN -7.85 dB
REM REPLAYGAIN_TRACK_PEAK 1.042687
INDEX 00 26:51:00
INDEX 01 26:52:37
"""
13 changes: 13 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ def test_one_file_many_tracks(
== obj_sample_one_file_many_tracks_strict
)

def test_real_cue(self, cue_sample_real: str):
res = cuetools.loads(cue_sample_real, strict_title_case=True)
assert res.performer == 'Scorpions', (
'Thats correct if it can parse the entire file without throwing an errors'
)


def test_line_parsing():
cue_sheet = """PERFORMER TITLE"""
Expand Down Expand Up @@ -113,3 +119,10 @@ def test_line_parsing():

cue = cuetools.loads(cue_sheet)
logger.debug(cue)

cue_sheet = """FILE "track.flac" WAVE
TRACK 01 AUDIO
INDEX 00 00:00:50
INDEX 01 00:00:65"""
cue = cuetools.loads(cue_sheet)
logger.debug(cue)