Skip to content

Commit

Permalink
Migrate CI to GitHub Actions (#21)
Browse files Browse the repository at this point in the history
* Migrate CI to Github Actions

* Fix python-version

* Add dev dependencies

* Ensure creation of directories

* Fix circular import

* Extend python-version

* Extend os

* Fix unsupported wurlitzer on Windows

* Simplify get_random_string

* Fix Windows paths

* Use joinpath

* New approach

* Add missing path
  • Loading branch information
kinuax authored May 8, 2024
1 parent 45905df commit dd5462b
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 100 deletions.
18 changes: 0 additions & 18 deletions .circleci/config.yml

This file was deleted.

33 changes: 33 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Tests

on:
pull_request:
branches: [main]
push:

jobs:
Tests:
name: ${{ matrix.os }} - ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12"]
fail-fast: false
steps:
- uses: actions/checkout@v4

- name: Install Poetry
run: pipx install poetry

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: poetry

- name: Install dependencies
run: poetry install --with dev

- name: Test
run: poetry run pytest -v
8 changes: 0 additions & 8 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion rolabesti/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .settings import max_overlap_length, tinydb_directory, tinydb_file
from .settings import max_overlap_length, tinydb_file
from .utils import get_settings, reset_settings, store_settings
22 changes: 17 additions & 5 deletions rolabesti/config/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from enum import Enum
from pathlib import Path
from typing import Type

from platformdirs import user_config_path, user_data_path, user_music_path, user_documents_path
Expand All @@ -14,10 +15,21 @@
from rolabesti.models import Sortings


def create_directories(directories: list[Path]) -> None:
"""Ensure directories are created."""
for path in directories:
if not path.exists():
path.mkdir(parents=True)


copy_path = user_documents_path()
music_path = user_music_path()
tinydb_path = user_data_path(__app_name__)
toml_path = user_config_path(__app_name__)
create_directories([copy_path, music_path, tinydb_path, toml_path])
tinydb_file = tinydb_path / "tracks.json"
toml_file = toml_path / "config.toml"
max_overlap_length = 30
tinydb_directory = user_data_path(__app_name__)
tinydb_file = tinydb_directory / "tracks.json"
toml_file = user_config_path(__app_name__) / "config.toml"


class Databases(str, Enum):
Expand All @@ -31,8 +43,8 @@ class Settings(BaseSettings):
max_tracklist_length: NonNegativeInt = 60
sorting: Sortings = Sortings.random
overlap_length: int = Field(3, ge=0, le=max_overlap_length)
music_directory: DirectoryPath = user_music_path()
copy_directory: DirectoryPath = user_documents_path()
music_directory: DirectoryPath = music_path
copy_directory: DirectoryPath = copy_path
database: Databases = Databases.tinydb
model_config = SettingsConfigDict(
toml_file=toml_file,
Expand Down
2 changes: 1 addition & 1 deletion rolabesti/controllers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __call__(self) -> None:
del self.parameters["list_"]
del self.parameters["reset"]

if all(map(lambda parameter: parameter is None, self.parameters.values())):
if all(parameter is None for parameter in self.parameters.values()):
self.logger.log("[green]no new settings to configure[/green]")
return

Expand Down
5 changes: 1 addition & 4 deletions rolabesti/controllers/controller.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os
from abc import ABC, abstractmethod

from rolabesti.config import get_settings, tinydb_directory, tinydb_file
from rolabesti.config import get_settings, tinydb_file
from rolabesti.database import TinyDB
from rolabesti.logger import Logger

Expand All @@ -14,8 +13,6 @@ def __init__(self, parameters: dict) -> None:
self.parameters = parameters
match settings.database:
case "tinydb":
if not tinydb_directory.exists():
os.mkdir(tinydb_directory)
self.db = TinyDB(tinydb_file)
self.logger = Logger()

Expand Down
70 changes: 39 additions & 31 deletions rolabesti/controllers/parser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import re
from pathlib import Path

from mutagen import MutagenError
Expand All @@ -9,56 +8,65 @@
from rolabesti.models import ID3Tags, Track


PATTERNS = {
r"/Places/([^/]+)/Genres/([^/]+)/Albums/([^/]+)/(?:([^/]+)/)?([^/]+)\.[mM][pP]3$": (
"place", "genre", "album", "side", "title"),
r"/Places/([^/]+)/Genres/([^/]+)/([^/]+)/([^/]+)/(?:([^/]+)/)?([^/]+)\.[mM][pP]3$": (
"place", "genre", "artist", "album", "side", "title"),
r"/Places/([^/]+)/Genres/([^/]+)/([^/]+)/([^/]+)\.[mM][pP]3$": ("place", "genre", "artist", "title"),
r"/Places/([^/]+)/Genres/([^/]+)/([^/]+)\.[mM][pP]3$": ("place", "genre", "title"),
r"/Places/([^/]+)/([^/]+)\.[mM][pP]3$": ("place", "title"),
r"/Places/([^/]+)/Albums/([^/]+)/(?:([^/]+)/)?([^/]+)\.[mM][pP]3$": ("place", "album", "side", "title"),
r"/Places/([^/]+)/([^/]+)/([^/]+)/(?:([^/]+)/)?([^/]+)\.[mM][pP]3$": ("place", "artist", "album", "side", "title"),
r"/Places/([^/]+)/([^/]+)/([^/]+)\.[mM][pP]3$": ("place", "artist", "title"),
r"/Genres/([^/]+)/Albums/([^/]+)/(?:([^/]+)/)?([^/]+)\.[mM][pP]3$": ("genre", "album", "side", "title"),
r"/Genres/([^/]+)/([^/]+)/([^/]+)/(?:([^/]+)/)?([^/]+)\.[mM][pP]3$": ("genre", "artist", "album", "side", "title"),
r"/Genres/([^/]+)/([^/]+)/([^/]+)\.[mM][pP]3$": ("genre", "artist", "title"),
r"/Genres/([^/]+)/([^/]+)\.[mM][pP]3$": ("genre", "title"),
r"/Artists/([^/]+)/([^/]+)/(?:([^/]+)/)?([^/]+)\.[mM][pP]3$": ("artist", "album", "side", "title"),
r"/Artists/([^/]+)/([^/]+)\.[mM][pP]3$": ("artist", "title"),
r"/Albums/([^/]+)/(?:([^/]+)/)?([^/]+)\.[mM][pP]3$": ("album", "side", "title"),
r"/([^/]+)\.[mM][pP]3$": ("title",),
}
patterns = [
[{"Places": -8, "Genres": -6, "Albums": -4}, {"place": -7, "genre": -5, "album": -3}],
[{"Places": -7, "Genres": -5, "Albums": -3}, {"place": -6, "genre": -4, "album": -2}],
[{"Places": -8, "Genres": -6}, {"place": -7, "genre": -5, "artist": -4, "album": -3}],
[{"Places": -7, "Genres": -5}, {"place": -6, "genre": -4, "artist": -3, "album": -2}],
[{"Places": -6, "Genres": -4}, {"place": -5, "genre": -3, "artist": -2}],
[{"Places": -5, "Genres": -3}, {"place": -4, "genre": -2}],
[{"Places": -6, "Albums": -4}, {"place": -5, "album": -3}],
[{"Places": -5, "Albums": -3}, {"place": -4, "album": -2}],
[{"Places": -6}, {"place": -5, "artist": -4, "album": -3}],
[{"Places": -5}, {"place": -4, "artist": -3, "album": -2}],
[{"Places": -4}, {"place": -3, "artist": -2}],
[{"Places": -3}, {"place": -2}],
[{"Genres": -6, "Albums": -4}, {"genre": -5, "album": -3}],
[{"Genres": -5, "Albums": -3}, {"genre": -4, "album": -2}],
[{"Genres": -6}, {"genre": -5, "artist": -4, "album": -3}],
[{"Genres": -5}, {"genre": -4, "artist": -3, "album": -2}],
[{"Genres": -4}, {"genre": -3, "artist": -2}],
[{"Genres": -3}, {"genre": -2}],
[{"Artists": -5}, {"artist": -4, "album": -3}],
[{"Artists": -4}, {"artist": -3, "album": -2}],
[{"Artists": -3}, {"artist": -2}],
[{"Albums": -4}, {"album": -3}],
[{"Albums": -3}, {"album": -2}],
]


class Parser:
"""Parsing related functionality."""

def parse(self, trackpath: Path) -> Track | None:
"""
Parse track located at trackpath and return a Track object.
If there is an error, return None.
"""
if (path_fields := self._parse_path_fields(trackpath)) is None:
return
if (id3_tags := self._parse_id3_tags(trackpath)) is None:
return
if (length := self._parse_length(trackpath)) is None:
return
path_fields = self._parse_path_fields(trackpath)
path_fields["title"] = trackpath.stem
if (track := self._build_track(trackpath, path_fields, id3_tags, length)) is None:
return
return track

@staticmethod
def _parse_path_fields(trackpath: Path) -> dict | None:
def _parse_path_fields(trackpath: Path) -> dict:
"""
Match and parse trackpath against PATTERNS.
Return a dictionary with parsed fields if there is a match.
If there is no matching pattern, return None.
Parse trackpath and return a dictionary with the path fields.
If there is no matching pattern, return an empty dictionary.
"""
for regex, fields in PATTERNS.items():
match = re.search(regex, str(trackpath))
if match:
return {field: value for field, value in zip(fields, match.groups()) if value}
for pattern in patterns:
try:
if all(field == trackpath.parts[index] for field, index in pattern[0].items()):
return {field: trackpath.parts[index] for field, index in pattern[1].items()}
except IndexError:
# Discard trackpath unable to match pattern.
pass
return {}

@staticmethod
def _parse_id3_tags(trackpath: Path) -> dict | None:
Expand Down
14 changes: 11 additions & 3 deletions rolabesti/views/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import contextlib
from contextlib import contextmanager, redirect_stdout
from pathlib import Path
from wurlitzer import pipes

try:
from wurlitzer import pipes
except ModuleNotFoundError:
# wurlitzer is not supported on Windows.
@contextmanager
def pipes():
yield


# Avoid messages from pygame while importing.
with contextlib.redirect_stdout(None):
with redirect_stdout(None):
import pygame


Expand Down
56 changes: 27 additions & 29 deletions tests/controllers/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,29 @@


@pytest.mark.parametrize("trackpath, length", [
(Path(f"/path/to/music/directory/Places/{place}/Genres/{genre}/Albums/{album}/{side}/{title}.mp3"), 5),
(Path(f"/path/to/music/directory/Places/{place}/Genres/{genre}/Albums/{album}/{title}.mp3"), 4),
(Path(f"/path/to/music/directory/Places/{place}/Genres/{genre}/{artist}/{album}/{side}/{title}.mp3"), 6),
(Path(f"/path/to/music/directory/Places/{place}/Genres/{genre}/{artist}/{album}/{title}.mp3"), 5),
(Path(f"/path/to/music/directory/Places/{place}/Genres/{genre}/{artist}/{title}.mp3"), 4),
(Path(f"/path/to/music/directory/Places/{place}/Genres/{genre}/{title}.mp3"), 3),
(Path(f"/path/to/music/directory/Places/{place}/Albums/{album}/{side}/{title}.mp3"), 4),
(Path(f"/path/to/music/directory/Places/{place}/Albums/{album}/{title}.mp3"), 3),
(Path(f"/path/to/music/directory/Places/{place}/{artist}/{album}/{side}/{title}.mp3"), 5),
(Path(f"/path/to/music/directory/Places/{place}/{artist}/{album}/{title}.mp3"), 4),
(Path(f"/path/to/music/directory/Places/{place}/{artist}/{title}.mp3"), 3),
(Path(f"/path/to/music/directory/Places/{place}/{title}.mp3"), 2),
(Path(f"/path/to/music/directory/Genres/{genre}/Albums/{album}/{side}/{title}.mp3"), 4),
(Path(f"/path/to/music/directory/Genres/{genre}/Albums/{album}/{title}.mp3"), 3),
(Path(f"/path/to/music/directory/Genres/{genre}/{artist}/{album}/{side}/{title}.mp3"), 5),
(Path(f"/path/to/music/directory/Genres/{genre}/{artist}/{album}/{title}.mp3"), 4),
(Path(f"/path/to/music/directory/Genres/{genre}/{artist}/{title}.mp3"), 3),
(Path(f"/path/to/music/directory/Genres/{genre}/{title}.mp3"), 2),
(Path(f"/path/to/music/directory/Albums/{album}/{side}/{title}.mp3"), 3),
(Path(f"/path/to/music/directory/Albums/{album}/{title}.mp3"), 2),
(Path(f"/path/to/music/directory/Artists/{artist}/{album}/{side}/{title}.mp3"), 4),
(Path(f"/path/to/music/directory/Artists/{artist}/{album}/{title}.mp3"), 3),
(Path(f"/path/to/music/directory/Artists/{artist}/{title}.mp3"), 2),
(Path(f"/path/to/music/directory/{title}.mp3"), 1),
(Path() / "Places" / place / "Genres" / genre / "Albums" / album / side / f"{title}.mp3", 3),
(Path() / "Places" / place / "Genres" / genre / "Albums" / album / f"{title}.mp3", 3),
(Path() / "Places" / place / "Genres" / genre / artist / album / side / f"{title}.mp3", 4),
(Path() / "Places" / place / "Genres" / genre / artist / album / f"{title}.mp3", 4),
(Path() / "Places" / place / "Genres" / genre / artist / f"{title}.mp3", 3),
(Path() / "Places" / place / "Genres" / genre / f"{title}.mp3", 2),
(Path() / "Places" / place / "Albums" / album / side / f"{title}.mp3", 2),
(Path() / "Places" / place / "Albums" / album / f"{title}.mp3", 2),
(Path() / "Places" / place / artist / album / side / f"{title}.mp3", 3),
(Path() / "Places" / place / artist / album / f"{title}.mp3", 3),
(Path() / "Places" / place / artist / f"{title}.mp3", 2),
(Path() / "Places" / place / f"{title}.mp3", 1),
(Path() / "Genres" / genre / "Albums" / album / side / f"{title}.mp3", 2),
(Path() / "Genres" / genre / "Albums" / album / f"{title}.mp3", 2),
(Path() / "Genres" / genre / artist / album / side / f"{title}.mp3", 3),
(Path() / "Genres" / genre / artist / album / f"{title}.mp3", 3),
(Path() / "Genres" / genre / artist / f"{title}.mp3", 2),
(Path() / "Genres" / genre / f"{title}.mp3", 1),
(Path() / "Albums" / album / side / f"{title}.mp3", 1),
(Path() / "Albums" / album / f"{title}.mp3", 1),
(Path() / "Artists" / artist / album / side / f"{title}.mp3", 2),
(Path() / "Artists" / artist / album / f"{title}.mp3", 2),
(Path() / "Artists" / artist / f"{title}.mp3", 1),
])
def test_parse_path_fields_with_supported_trackpaths(
trackpath: Path,
Expand All @@ -54,13 +53,12 @@ def test_parse_path_fields_with_supported_trackpaths(


@pytest.mark.parametrize("trackpath", [
(Path(f"/path/to/music/directory/Places/{place}/Genre/{genre}"), ),
(Path(f"/path/to/music/directory/Places/{place}/Genre/{genre}/{title}"), ),
(Path(f"/path/to/music/directory/Places/{place}/{title}.pdf"), ),
(Path("@#wW#$DS23VTW#@%$wsVWExEW234ER^#^#$%"), ),
Path() / "Places" / place,
Path() / "Albums" / album,
Path() / "some" / "path",
])
def test_parse_path_fields_with_unsupported_trackpaths(
trackpath: Path,
) -> None:
path_fields = parser._parse_path_fields(trackpath)
assert path_fields is None
assert path_fields == {}

0 comments on commit dd5462b

Please sign in to comment.