diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a71c8e2..8f22746 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,10 @@ name: CI -on: [push, pull_request] +on: + pull_request: + push: + branches: + - main jobs: main: @@ -8,45 +12,40 @@ jobs: fail-fast: false matrix: include: - - name: "Test: Python 3.7" - python: "3.7" - tox: py37 - - name: "Test: Python 3.8" - python: "3.8" - tox: py38 - name: "Test: Python 3.9" python: "3.9" tox: py39 + - name: "Test: Python 3.10" + python: "3.10" + tox: py310 + - name: "Test: Python 3.11" + python: "3.11" + tox: py311 coverage: true - name: "Lint: check-manifest" - python: "3.9" + python: "3.11" tox: check-manifest - name: "Lint: flake8" - python: "3.9" + python: "3.11" tox: flake8 name: ${{ matrix.name }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 container: ghcr.io/mopidy/ci:latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} + - uses: actions/checkout@v3 - name: Fix home dir permissions to enable pip caching run: chown -R root /github/home - - name: Cache pip - uses: actions/cache@v2 + - uses: actions/setup-python@v4 with: - path: ~/.cache/pip - key: ${{ runner.os }}-${{ matrix.python }}-${{ matrix.tox }}-pip-${{ hashFiles('setup.cfg') }}-${{ hashFiles('tox.ini') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.python }}-${{ matrix.tox }}-pip- + python-version: ${{ matrix.python }} + cache: pip + cache-dependency-path: setup.cfg - run: python -m pip install pygobject tox - run: python -m tox -e ${{ matrix.tox }} if: ${{ ! matrix.coverage }} - run: python -m tox -e ${{ matrix.tox }} -- --cov-report=xml if: ${{ matrix.coverage }} - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 if: ${{ matrix.coverage }} diff --git a/README.rst b/README.rst index 5f1c257..b784a04 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ Mopidy-Local :target: https://pypi.org/project/Mopidy-Local/ :alt: Latest PyPI version -.. image:: https://img.shields.io/github/workflow/status/mopidy/mopidy-local/CI +.. image:: https://img.shields.io/github/actions/workflow/status/mopidy/mopidy-local/ci.yml?branch=main :target: https://github.com/mopidy/mopidy-local/actions :alt: CI build status diff --git a/mopidy_local/__init__.py b/mopidy_local/__init__.py index adb5c5c..bc5a72b 100644 --- a/mopidy_local/__init__.py +++ b/mopidy_local/__init__.py @@ -23,7 +23,9 @@ def get_config_schema(self): schema["data_dir"] = config.Deprecated() schema["playlists_dir"] = config.Deprecated() schema["tag_cache_file"] = config.Deprecated() - schema["scan_timeout"] = config.Integer(minimum=1000, maximum=1000 * 60 * 60) + schema["scan_timeout"] = config.Integer( + minimum=1000, maximum=1000 * 60 * 60 + ) schema["scan_flush_threshold"] = config.Integer(minimum=0) schema["scan_follow_symlinks"] = config.Boolean() schema["included_file_extensions"] = config.List(optional=True) @@ -38,7 +40,9 @@ def setup(self, registry): from .actor import LocalBackend registry.add("backend", LocalBackend) - registry.add("http:app", {"name": self.ext_name, "factory": self.webapp}) + registry.add( + "http:app", {"name": self.ext_name, "factory": self.webapp} + ) def get_command(self): from .commands import LocalCommand diff --git a/mopidy_local/commands.py b/mopidy_local/commands.py index b8f851f..a5a8667 100644 --- a/mopidy_local/commands.py +++ b/mopidy_local/commands.py @@ -107,7 +107,9 @@ def run(self, args, config): def _find_files(self, *, media_dir, follow_symlinks): logger.info(f"Finding files in {media_dir.as_uri()} ...") - file_mtimes, file_errors = mtimes.find_mtimes(media_dir, follow=follow_symlinks) + file_mtimes, file_errors = mtimes.find_mtimes( + media_dir, follow=follow_symlinks + ) logger.info(f"Found {len(file_mtimes)} files in {media_dir.as_uri()}") if file_errors: @@ -169,7 +171,9 @@ def _extension_filters( ): if included_file_exts: if relative_path.suffix.lower() in included_file_exts: - logger.debug(f"Added {file_uri}: File extension on included list") + logger.debug( + f"Added {file_uri}: File extension on included list" + ) return True else: logger.debug( @@ -178,7 +182,9 @@ def _extension_filters( return False else: if relative_path.suffix.lower() in excluded_file_exts: - logger.debug(f"Skipped {file_uri}: File extension on excluded list") + logger.debug( + f"Skipped {file_uri}: File extension on excluded list" + ) return False else: logger.debug( @@ -193,17 +199,30 @@ def _extension_filters( if ( not _is_hidden_file(relative_path, file_uri) and _extension_filters( - relative_path, file_uri, included_file_exts, excluded_file_exts + relative_path, + file_uri, + included_file_exts, + excluded_file_exts, ) and absolute_path not in files_in_library ): files_to_update.add(absolute_path) - logger.info(f"Found {len(files_to_update)} tracks which need to be updated") + logger.info( + f"Found {len(files_to_update)} tracks which need to be updated" + ) return files_to_update def _scan_metadata( - self, *, media_dir, file_mtimes, files, library, timeout, flush_threshold, limit + self, + *, + media_dir, + file_mtimes, + files, + library, + timeout, + flush_threshold, + limit, ): logger.info("Scanning...") @@ -237,7 +256,9 @@ def _scan_metadata( ) mtime = file_mtimes.get(absolute_path) track = tags.convert_tags_to_track(result.tags).replace( - uri=local_uri, length=result.duration, last_modified=mtime + uri=local_uri, + length=result.duration, + last_modified=mtime, ) library.add(track, result.tags, result.duration) logger.debug(f"Added {track.uri}") diff --git a/mopidy_local/library.py b/mopidy_local/library.py index c8c3ca7..afaacf6 100644 --- a/mopidy_local/library.py +++ b/mopidy_local/library.py @@ -13,7 +13,8 @@ def date_ref(date): return Ref.directory( - uri=uritools.uricompose("local", None, "directory", {"date": date}), name=date + uri=uritools.uricompose("local", None, "directory", {"date": date}), + name=date, ) @@ -27,7 +28,9 @@ def genre_ref(genre): class LocalLibraryProvider(backend.LibraryProvider): ROOT_DIRECTORY_URI = "local:directory" - root_directory = models.Ref.directory(uri=ROOT_DIRECTORY_URI, name="Local media") + root_directory = models.Ref.directory( + uri=ROOT_DIRECTORY_URI, name="Local media" + ) def __init__(self, backend, config): super().__init__(backend) @@ -152,9 +155,13 @@ def _browse_directory(self, uri, order=("type", "name COLLATE NOCASE")): # TODO: handle these in schema (generically)? if type == "date": format = query.get("format", "%Y-%m-%d") - return list(map(date_ref, schema.dates(self._connect(), format=format))) + return list( + map(date_ref, schema.dates(self._connect(), format=format)) + ) if type == "genre": - return list(map(genre_ref, schema.list_distinct(self._connect(), "genre"))) + return list( + map(genre_ref, schema.list_distinct(self._connect(), "genre")) + ) # Fix #38: keep sort order of album tracks; this also applies # to composers and performers @@ -186,7 +193,10 @@ def _browse_directory(self, uri, order=("type", "name COLLATE NOCASE")): refs.append( Ref.directory( uri=uritools.uricompose( - "local", None, "directory", dict(query, **{role: ref.uri}) + "local", + None, + "directory", + dict(query, **{role: ref.uri}), ), name=ref.name, ) diff --git a/mopidy_local/schema.py b/mopidy_local/schema.py index 8ebe6d2..affb509 100644 --- a/mopidy_local/schema.py +++ b/mopidy_local/schema.py @@ -258,7 +258,10 @@ def exists(c, uri): def browse(c, type=None, order=("type", "name COLLATE NOCASE"), **kwargs): filters, params = _filters(_BROWSE_FILTERS[type], **kwargs) - sql = _BROWSE_QUERIES[type] % (" AND ".join(filters) or "1", ", ".join(order)) + sql = _BROWSE_QUERIES[type] % ( + " AND ".join(filters) or "1", + ", ".join(order), + ) logger.debug("SQLite browse query %r: %s", params, sql) return [Ref(**row) for row in c.execute(sql, params)] diff --git a/mopidy_local/storage.py b/mopidy_local/storage.py index f700941..134b8a7 100644 --- a/mopidy_local/storage.py +++ b/mopidy_local/storage.py @@ -159,7 +159,9 @@ def _validate_album(self, model): raise ValueError("Empty album name") if not model.uri: model = model.replace(uri=model_uri("album", model)) - return model.replace(artists=list(map(self._validate_artist, model.artists))) + return model.replace( + artists=list(map(self._validate_artist, model.artists)) + ) def _validate_track(self, model): if not model.uri: @@ -237,6 +239,8 @@ def _get_or_create_image_file(self, path, data=None): name = f"{digest}.{what}" image_path = self._image_dir / name if not image_path.is_file(): - logger.info(f"Creating file {image_path.as_uri()} from {data_source}") + logger.info( + f"Creating file {image_path.as_uri()} from {data_source}" + ) image_path.write_bytes(data) return uritools.urijoin(self._base_uri, name) diff --git a/mopidy_local/translator.py b/mopidy_local/translator.py index 7792b73..f2b9346 100644 --- a/mopidy_local/translator.py +++ b/mopidy_local/translator.py @@ -32,7 +32,9 @@ def path_to_file_uri(path: Union[str, bytes, Path]) -> str: return ppath.as_uri() -def path_to_local_track_uri(path: Union[str, bytes, Path], media_dir: Path) -> str: +def path_to_local_track_uri( + path: Union[str, bytes, Path], media_dir: Path +) -> str: """Convert path to local track URI.""" ppath = Path(os.fsdecode(path)) if ppath.is_absolute(): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..04a6524 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools >= 30.3.0", "wheel"] + + +[tool.black] +target-version = ["py39", "py310", "py311"] +line-length = 80 + + +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +line_length = 88 +known_tests = "tests" +sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,TESTS,LOCALFOLDER" diff --git a/setup.cfg b/setup.cfg index 25db4cf..0284360 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,9 +14,9 @@ classifiers = License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Topic :: Multimedia :: Sound/Audio :: Players @@ -24,7 +24,7 @@ classifiers = zip_safe = False include_package_data = True packages = find: -python_requires = >= 3.7 +python_requires = >= 3.9 install_requires = Mopidy >= 3.0.0 Pykka >= 2.0.1 diff --git a/tests/test_library.py b/tests/test_library.py index b831f1a..d2d12dc 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -13,7 +13,10 @@ class LocalLibraryProviderTest(unittest.TestCase): config = { - "core": {"data_dir": path_to_data_dir(""), "max_tracklist_length": 10000}, + "core": { + "data_dir": path_to_data_dir(""), + "max_tracklist_length": 10000, + }, "local": { "media_dir": path_to_data_dir(""), "directories": [], @@ -45,7 +48,9 @@ def tearDown(self): # noqa: N802 def test_add_noname_ascii(self): name = "Test.mp3" - uri = translator.path_to_local_track_uri(name, pathlib.Path("/media/dir")) + uri = translator.path_to_local_track_uri( + name, pathlib.Path("/media/dir") + ) track = Track(name=name, uri=uri) self.storage.begin() self.storage.add(track) diff --git a/tests/test_playback.py b/tests/test_playback.py index e23fce3..c0a51fa 100644 --- a/tests/test_playback.py +++ b/tests/test_playback.py @@ -8,14 +8,22 @@ from mopidy_local import actor from unittest import mock -from tests import dummy_audio, generate_song, path_to_data_dir, populate_tracklist +from tests import ( + dummy_audio, + generate_song, + path_to_data_dir, + populate_tracklist, +) # TODO Test 'playlist repeat', e.g. repeat=1,single=0 class LocalPlaybackProviderTest(unittest.TestCase): config = { - "core": {"data_dir": path_to_data_dir(""), "max_tracklist_length": 10000}, + "core": { + "data_dir": path_to_data_dir(""), + "max_tracklist_length": 10000, + }, "local": { "media_dir": path_to_data_dir(""), "directories": [], @@ -552,7 +560,9 @@ def test_end_of_track_with_random(self, shuffle_mock): @populate_tracklist @mock.patch("random.shuffle") - def test_end_of_track_track_with_random_after_append_playlist(self, shuffle_mock): + def test_end_of_track_track_with_random_after_append_playlist( + self, shuffle_mock + ): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.set_random(True) diff --git a/tests/test_schema.py b/tests/test_schema.py index d11947c..6594a87 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -10,19 +10,43 @@ class SchemaTest(unittest.TestCase): artists = [ - Artist(uri="local:artist:0", name="artist #0", musicbrainz_id="1234a-987c"), + Artist( + uri="local:artist:0", name="artist #0", musicbrainz_id="1234a-987c" + ), Artist(uri="local:artist:1", name="artist #1"), ] albums = [ - Album(uri="local:album:0", name="album #0", musicbrainz_id="1234a-3421d"), + Album( + uri="local:album:0", name="album #0", musicbrainz_id="1234a-3421d" + ), Album(uri="local:album:1", name="album #1", artists=[artists[0]]), Album(uri="local:album:2", name="album #2", artists=[artists[1]]), ] tracks = [ - Track(uri="local:track:0", name="track #0", date="2015-03-15", genre="Rock"), - Track(uri="local:track:1", name="track #1", date="2014", artists=[artists[0]]), - Track(uri="local:track:2", name="track #2", date="2020-10", album=albums[0]), - Track(uri="local:track:3", name="track #3", date="2020-10-01", album=albums[1]), + Track( + uri="local:track:0", + name="track #0", + date="2015-03-15", + genre="Rock", + ), + Track( + uri="local:track:1", + name="track #1", + date="2014", + artists=[artists[0]], + ), + Track( + uri="local:track:2", + name="track #2", + date="2020-10", + album=albums[0], + ), + Track( + uri="local:track:3", + name="track #3", + date="2020-10-01", + album=albums[1], + ), Track( uri="local:track:4", name="track #4", @@ -81,7 +105,8 @@ def test_list_distinct(self): schema.list_distinct(self.connection, "date"), ) self.assertEqual( - [self.tracks[0].genre], schema.list_distinct(self.connection, "genre") + [self.tracks[0].genre], + schema.list_distinct(self.connection, "genre"), ) self.assertEqual( [self.tracks[4].musicbrainz_id], @@ -158,7 +183,9 @@ def test_indexed_search(self): ]: for exact in (True, False): with self.connection as c: - tracks = schema.search_tracks(c, query, 10, 0, exact, filters) + tracks = schema.search_tracks( + c, query, 10, 0, exact, filters + ) self.assertCountEqual(results, map(lambda t: t.uri, tracks)) def test_fulltext_search(self): diff --git a/tests/test_tracklist.py b/tests/test_tracklist.py index 3d7eb3b..e97e468 100644 --- a/tests/test_tracklist.py +++ b/tests/test_tracklist.py @@ -7,12 +7,20 @@ from mopidy.models import Playlist, Track from mopidy_local import actor -from tests import dummy_audio, generate_song, path_to_data_dir, populate_tracklist +from tests import ( + dummy_audio, + generate_song, + path_to_data_dir, + populate_tracklist, +) class LocalTracklistProviderTest(unittest.TestCase): config = { - "core": {"data_dir": path_to_data_dir(""), "max_tracklist_length": 10000}, + "core": { + "data_dir": path_to_data_dir(""), + "max_tracklist_length": 10000, + }, "local": { "media_dir": path_to_data_dir(""), "directories": [], @@ -114,7 +122,9 @@ def test_filter_by_uri_returns_multiple_matches(self): assert track == tl_tracks[1].track def test_filter_by_uri_returns_nothing_if_no_match(self): - self.controller.playlist = Playlist(tracks=[Track(uri="z"), Track(uri="y")]) + self.controller.playlist = Playlist( + tracks=[Track(uri="z"), Track(uri="y")] + ) assert [] == self.controller.filter({"uri": ["a"]}).get() def test_filter_by_multiple_criteria_returns_elements_matching_all(self): diff --git a/tox.ini b/tox.ini index 11be5e5..ec686f9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37, py38, py39, check-manifest, flake8 +envlist = py39, py310, py311, check-manifest, flake8 [testenv] sitepackages = true