Skip to content
Merged

Dev #19

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
13 changes: 10 additions & 3 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ name: Run Tests on PR
on:
pull_request:
branches: [ main ]
paths-ignore:
- 'README.md'

jobs:
test:
ci:
runs-on: ubuntu-latest

env:
Expand All @@ -28,7 +30,8 @@ jobs:
--health-retries 5

steps:
- uses: actions/checkout@v4
- name: Get repository code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
Expand All @@ -47,9 +50,13 @@ jobs:
run: |
poetry run alembic upgrade head

- name: Run Mypy check
run: |
poetry run mypy .

- name: Run tests and generate coverage report
run: |
poetry run pytest --cov=audiostats --cov-report=lcov tests/
poetry run pytest --cov-report=lcov tests/

- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@master
Expand Down
2 changes: 1 addition & 1 deletion audiostats/db/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
class DBApi:
def __init__(self, db_url : str, workers : int = 5, queue_sz : int = 10):
self._session_factory = SessionFactory(db_url)
self._queue = asyncio.Queue(maxsize=queue_sz)
self._queue: asyncio.Queue[AlbumDTO | None] = asyncio.Queue(maxsize=queue_sz)
self._num_workers = workers

async def _album_upserter(self):
Expand Down
7 changes: 4 additions & 3 deletions audiostats/db/models.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from datetime import datetime

from sqlalchemy import String, Integer, UniqueConstraint, ForeignKey, Float, Enum, DateTime, func
from sqlalchemy.orm import declarative_base, Mapped, mapped_column, relationship
from sqlalchemy.orm import Mapped, mapped_column, relationship, DeclarativeBase

from audiostats.domain import Status, Success

Base = declarative_base()

MAX_PATH_FIELD_LEN = 200
MAX_STR_FIELD_LEN = 50

class Base(DeclarativeBase):
...

class Album(Base):
"""Represents **albums** table line as orm object

Expand Down
25 changes: 13 additions & 12 deletions audiostats/db/session.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from asyncio import Semaphore
from contextlib import asynccontextmanager
from typing import AsyncIterator

from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession

Expand All @@ -17,7 +18,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
await self._engine.dispose()

@asynccontextmanager
async def get_session(self) -> AsyncSession:
async def get_session(self) -> AsyncIterator[AsyncSession]:
session = None
try:
await self._semaphore.acquire()
Expand All @@ -30,14 +31,14 @@ async def get_session(self) -> AsyncSession:
await session.close()
self._semaphore.release()

async def __call__(self, *args, **kwargs) -> AsyncSession:
await self._semaphore.acquire()
session = self._session_maker()

async def close_session():
await session.close()
self._semaphore.release()

session.close = close_session()

return session
# async def __call__(self, *args, **kwargs) -> AsyncSession:
# await self._semaphore.acquire()
# session = self._session_maker()
#
# async def close_session():
# await session.close()
# self._semaphore.release()
#
# session.close = close_session()
#
# return session
32 changes: 22 additions & 10 deletions audiostats/handlers/plst_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@
import subprocess
import logging

from typing import Any
from typing import Any, TYPE_CHECKING
from collections.abc import Iterator
from cuetools import TrackData
from types import ModuleType

from .models import AlbumDTO, TrackDTO

librosa: ModuleType | None = None
LIBROSA_AVAILABLE = False

try:
import librosa
#import librosa
import librosa as _librosa
librosa = _librosa

LIBROSA_AVAILABLE = True
except ImportError:
librosa = None
LIBROSA_AVAILABLE = False
pass

MIN_TRACK_DURATION = 10 #Used to decide whether there are more tracks in the file or whether a new file should be started

Expand Down Expand Up @@ -78,8 +85,10 @@ def _process_cue(self, path: str) -> AlbumDTO | None:
return album
except FileNotFoundError:
logger.warning(f'No such file: {path}')
return None
except UnicodeDecodeError:
logger.warning(f'Cant read file: {path}')
return None


def _get_cover_path(self, current_dir: str) -> str | None:
Expand All @@ -90,29 +99,32 @@ def _get_cover_path(self, current_dir: str) -> str | None:
return None

def _process_cue_tracks(self, cue : cuetools.AlbumData, current_dir : str) -> Iterator[TrackDTO]:
offset = 0
offset: float = 0
duration: float
for track_cue in sorted(cue.tracks, reverse=True, key=lambda x: int(x.track)):
offset, duration = self._get_offset_duration(current_dir, track_cue, offset) if LIBROSA_AVAILABLE else (None, None)
offset, duration = self._get_offset_duration(current_dir, track_cue, offset) if LIBROSA_AVAILABLE else (0.0,0.0)
if not (title:=track_cue.title):
logger.warning(f'No title in track: {track_cue.track}]')
else:
track = TrackDTO(title=title,
number=int(track_cue.track),
path=os.path.join(current_dir, track_cue.link),
offset=offset,
duration=duration)
offset=offset if LIBROSA_AVAILABLE else None,
duration=duration if LIBROSA_AVAILABLE else None)
yield track

def _get_offset_duration(self, current_dir : str, track_cue : TrackData, next_offset : float | None) -> tuple[float, float]:
offset = frame_t_sec(track_cue.index['01']) if track_cue.index['01'] else 0
duration = next_offset - offset if next_offset >= MIN_TRACK_DURATION else self._get_audiofile_duration(os.path.join(current_dir,track_cue.link)) - offset
duration = next_offset - offset if next_offset is not None and next_offset >= MIN_TRACK_DURATION else self._get_audiofile_duration(os.path.join(current_dir,track_cue.link)) - offset
return offset, duration

def _get_audiofile_duration(self, path_to_file: str) -> float:
if path_to_file.endswith('.ape'):
return self._get_ape_duration(path_to_file)
else:
return librosa.get_duration(path=path_to_file)
if librosa is not None:
return librosa.get_duration(path=path_to_file)
raise RuntimeError('librosa is unavailable')

def _get_ape_duration(self, path_to_file: str) -> float:
result = subprocess.run(
Expand Down
6 changes: 5 additions & 1 deletion migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
# access to the values within the .ini file in use.
config = context.config

config.set_main_option('sqlalchemy.url', os.getenv('SYNC_DB_URL'))
# get config from env
sync_db_url = os.getenv('SYNC_DB_URL')
if not sync_db_url:
raise ValueError('SYNC_DB_URL env variable is required for Alembic')
config.set_main_option('sqlalchemy.url', sync_db_url)


# Interpret the config file for Python logging.
Expand Down
88 changes: 86 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ pytest = "^8.4.1"
pytest-cov = "^6.2.1"
pytest-asyncio = "^1.1.0"
dotenv = "^0.9.9"
mypy = "^1.18.2"

[tool.mypy]
python_version = "3.12"
ignore_missing_imports = true

[tool.pytest.ini_options]
testpaths = ["tests/"]
Expand Down