Skip to content

Commit

Permalink
merge_playlists now updates paths for local merges + name and path no…
Browse files Browse the repository at this point in the history
…w settable on local playlists and library
  • Loading branch information
geo-martino committed Jul 25, 2024
1 parent c63be5a commit 0083d47
Show file tree
Hide file tree
Showing 21 changed files with 3,057 additions and 2,903 deletions.
60 changes: 30 additions & 30 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
VERSION = $(shell hatch version)
SPHINXOPTS ?= -D release=${VERSION}
SPHINXBUILD ?= sphinx-build
SOURCEDIR = docs
BUILDDIR = docs/_build
PROJECTNAME = musify
LINKCHECKDIR = docs/_linkcheck

# Put it first so that "make" without argument is like "make help".
help:
$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

rebuild-html: Makefile
@$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@rm -f "$(SOURCEDIR)"/reference/"$(PROJECTNAME)"*.rst
@sphinx-apidoc -o "$(SOURCEDIR)"/reference ./"$(PROJECTNAME)" -d 4 --force --module-first --separate --no-toc -t "$(SOURCEDIR)"/_templates
@$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@$(SPHINXBUILD) -b linkcheck "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
VERSION = $(shell hatch version)
SPHINXOPTS ?= -D release=${VERSION}
SPHINXBUILD ?= sphinx-build
SOURCEDIR = docs
BUILDDIR = docs/_build
PROJECTNAME = musify
LINKCHECKDIR = docs/_linkcheck

# Put it first so that "make" without argument is like "make help".
help:
$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

rebuild-html: Makefile
@$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@rm -f "$(SOURCEDIR)"/reference/"$(PROJECTNAME)"*.rst
@sphinx-apidoc -o "$(SOURCEDIR)"/reference ./"$(PROJECTNAME)" -d 4 --force --module-first --separate --no-toc -t "$(SOURCEDIR)"/_templates
@$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@$(SPHINXBUILD) -b linkcheck "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
10 changes: 5 additions & 5 deletions docs/info/licence.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Licence
=======

.. literalinclude:: /../LICENSE
:language: none
Licence
=======

.. literalinclude:: /../LICENSE
:language: none
13 changes: 13 additions & 0 deletions docs/info/release-history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ Release History
The format is based on `Keep a Changelog <https://keepachangelog.com/en>`_,
and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_

1.1.4
=====

Added
-----
* :py:class:`.LocalPlaylist` now allows setting of the ``path`` property
* :py:class:`.LocalLibrary` now allows setting of the ``name`` property. Added ``name`` as an init parameter too.

Changed
-------
* :py:meth:`.LocalLibrary.merge_playlists` now updates the path of new playlists added to the library to be relative
to the library's ``playlist_folder``


1.1.3
=====
Expand Down
15 changes: 6 additions & 9 deletions musify/libraries/core/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ def __ior__(self, other: Playlist[T]) -> Self:
return self


type LibraryMergeType[T] = Library[T] | Collection[Playlist[T]] | Mapping[str, Playlist[T]]


class Library[T: Track](MusifyCollection[T], metaclass=ABCMeta):
"""A library of items and playlists and other object types."""

Expand Down Expand Up @@ -375,11 +378,7 @@ def log_playlists(self) -> None:
"""Log stats on currently loaded playlists"""
raise NotImplementedError

def merge_playlists(
self,
playlists: Library[T] | Collection[Playlist[T]] | Mapping[str, Playlist[T]],
reference: Library[T] | Collection[Playlist[T]] | Mapping[str, Playlist[T]] | None = None,
) -> None:
def merge_playlists(self, playlists: LibraryMergeType[T], reference: LibraryMergeType[T] | None = None) -> None:
"""
Merge playlists from given list/map/library to this library.
Expand All @@ -392,9 +391,7 @@ def merge_playlists(
this playlist based on the reference. Useful for using this function as a synchronizer
where the reference refers to the playlist at the previous sync.
"""
def get_playlists_map(
value: Library[T] | Collection[Playlist[T]] | Mapping[str, Playlist[T]]
) -> Mapping[str, Playlist[T]]:
def get_playlists_map(value: LibraryMergeType[T]) -> Mapping[str, Playlist[T]]:
"""Reformat the input playlist values to map"""
if isinstance(value, Mapping):
return value
Expand All @@ -409,7 +406,7 @@ def get_playlists_map(

for name, playlist in playlists.items():
if name not in self.playlists:
self.playlists[name] = playlist
self.playlists[name] = deepcopy(playlist)
continue

self.playlists[name].merge(playlist, reference=reference.get(name))
Expand Down
36 changes: 30 additions & 6 deletions musify/libraries/local/library/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from musify.base import Result
from musify.exception import MusifyError
from musify.file.path_mapper import PathMapper, PathStemMapper
from musify.libraries.core.object import Library
from musify.libraries.core.object import Library, LibraryMergeType
from musify.libraries.local.collection import LocalCollection, LocalFolder, LocalAlbum, LocalArtist, LocalGenres
from musify.libraries.local.playlist import PLAYLIST_CLASSES, LocalPlaylist, load_playlist
from musify.libraries.local.track import TRACK_CLASSES, LocalTrack, load_track
Expand Down Expand Up @@ -49,9 +49,11 @@ class LocalLibrary(LocalCollection[LocalTrack], Library[LocalTrack]):
If given, the wrangler can be used when calling __get_item__ to get an item from the collection from its URI.
The wrangler is also used when loading tracks to allow them to process URI tags.
For more info on this, see :py:class:`LocalTrack`.
:param name: A name to assign to this library.
"""

__slots__ = (
"_name",
"_library_folders",
"_playlist_folder",
"playlist_filter",
Expand All @@ -65,12 +67,13 @@ class LocalLibrary(LocalCollection[LocalTrack], Library[LocalTrack]):
__attributes_classes__ = (Library, LocalCollection)
__attributes_ignore__ = ("tracks_in_playlists",)

# noinspection PyPropertyDefinition
@classmethod
@property
def name(cls) -> str:
"""The type of library loaded"""
return str(cls.source)
def name(self) -> str:
return self._name

@name.setter
def name(self, value: str):
self._name = value

# noinspection PyPropertyDefinition
@classmethod
Expand Down Expand Up @@ -229,9 +232,12 @@ def __init__(
playlist_filter: Collection[str] | Filter[str] = (),
path_mapper: PathMapper = PathMapper(),
remote_wrangler: RemoteDataWrangler | None = None,
name: str = None,
):
super().__init__(remote_wrangler=remote_wrangler)

self._name: str = name if name else self.source

self.logger.debug(f"Setup {self.name} library: START")
self.logger.info(f"\33[1;95m ->\33[1;97m Setting up {self.name} library \33[0m")
self.logger.print_line()
Expand Down Expand Up @@ -411,6 +417,24 @@ async def _save_playlist(pl: LocalPlaylist) -> tuple[LocalPlaylist, Result]:
)
return dict([await _save_playlist(pl) for pl in bar])

def merge_playlists(
self, playlists: LibraryMergeType[LocalTrack], reference: LibraryMergeType[LocalTrack] | None = None
) -> None:
current_names = set(self.playlists)

super().merge_playlists(playlists=playlists, reference=reference)

for pl in self.playlists.values():
if pl.name in current_names:
continue

if isinstance(playlists, LocalLibrary):
rel_path = pl.path.relative_to(playlists.playlist_folder)
else:
rel_path = pl.path.name

pl.path = self.playlist_folder.joinpath(rel_path)

###########################################################################
## Backup/restore
###########################################################################
Expand Down
3 changes: 3 additions & 0 deletions musify/libraries/local/library/musicbee.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class MusicBee(LocalLibrary, File):
If given, the wrangler can be used when calling __get_item__ to get an item from the collection from its URI.
The wrangler is also used when loading tracks to allow them to process URI tags.
For more info on this, see :py:class:`LocalTrack`.
:param name: A name to assign to this library.
"""

__slots__ = (
Expand Down Expand Up @@ -85,6 +86,7 @@ def __init__(
playlist_filter: Collection[str] | Filter[str] = (),
path_mapper: PathMapper = PathMapper(),
remote_wrangler: RemoteDataWrangler = None,
name: str = None,
):
required_modules_installed(REQUIRED_MODULES, self)

Expand Down Expand Up @@ -129,6 +131,7 @@ def __init__(
playlist_filter=playlist_filter,
path_mapper=path_mapper,
remote_wrangler=remote_wrangler,
name=name,
)

def _get_track_from_xml_path(
Expand Down
4 changes: 4 additions & 0 deletions musify/libraries/local/playlist/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ def tracks(self, value: list[LocalTrack]):
def path(self):
return self._path

@path.setter
def path(self, value: str | Path):
self._path = Path(value)

def __init__(
self,
path: str | Path,
Expand Down
108 changes: 54 additions & 54 deletions musify/libraries/remote/core/_response.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,54 @@
"""
Just the core abstract class for the :py:mod:`Remote` module.
Placed here separately to avoid circular import logic issues.
"""
from abc import ABCMeta, abstractmethod
from typing import Any

from yarl import URL

from musify.base import MusifyObject


class RemoteResponse(MusifyObject, metaclass=ABCMeta):

__slots__ = ()

# noinspection PyPropertyDefinition,PyMethodParameters
@property
@abstractmethod
def kind(cls):
"""The type of remote object this class represents"""
raise NotImplementedError

@property
@abstractmethod
def response(self) -> dict[str, Any]:
"""The API response for this object"""
raise NotImplementedError

@property
@abstractmethod
def id(self) -> str:
"""The ID of this item/collection."""
raise NotImplementedError

@property
@abstractmethod
def url(self) -> URL:
"""The API URL of this item/collection."""
raise NotImplementedError

@property
@abstractmethod
def url_ext(self) -> URL | None:
"""The external URL of this item/collection."""
raise NotImplementedError

@abstractmethod
def refresh(self, skip_checks: bool = False) -> None:
"""
Refresh this object by updating from the stored API response.
Useful for updating stored variables after making changes to the stored API response manually.
"""
raise NotImplementedError
"""
Just the core abstract class for the :py:mod:`Remote` module.
Placed here separately to avoid circular import logic issues.
"""
from abc import ABCMeta, abstractmethod
from typing import Any

from yarl import URL

from musify.base import MusifyObject


class RemoteResponse(MusifyObject, metaclass=ABCMeta):

__slots__ = ()

# noinspection PyPropertyDefinition,PyMethodParameters
@property
@abstractmethod
def kind(cls):
"""The type of remote object this class represents"""
raise NotImplementedError

@property
@abstractmethod
def response(self) -> dict[str, Any]:
"""The API response for this object"""
raise NotImplementedError

@property
@abstractmethod
def id(self) -> str:
"""The ID of this item/collection."""
raise NotImplementedError

@property
@abstractmethod
def url(self) -> URL:
"""The API URL of this item/collection."""
raise NotImplementedError

@property
@abstractmethod
def url_ext(self) -> URL | None:
"""The external URL of this item/collection."""
raise NotImplementedError

@abstractmethod
def refresh(self, skip_checks: bool = False) -> None:
"""
Refresh this object by updating from the stored API response.
Useful for updating stored variables after making changes to the stored API response manually.
"""
raise NotImplementedError
Loading

0 comments on commit 0083d47

Please sign in to comment.