286 lines
9.9 KiB
Python
286 lines
9.9 KiB
Python
import logging
|
|
from concurrent.futures import Future, ThreadPoolExecutor
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import (
|
|
Any,
|
|
Callable,
|
|
Generic,
|
|
Optional,
|
|
Sequence,
|
|
Set,
|
|
Type,
|
|
TypeVar,
|
|
Union,
|
|
)
|
|
|
|
from sublime.config import AppConfiguration
|
|
|
|
from .adapter_base import Adapter, CacheMissError, CachingAdapter
|
|
from .api_objects import Playlist, PlaylistDetails
|
|
from .filesystem import FilesystemAdapter
|
|
from .subsonic import SubsonicAdapter
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
class Result(Generic[T]):
|
|
"""
|
|
A result from a :class:`AdapterManager` function. This is effectively a wrapper
|
|
around a :class:`concurrent.futures.Future`, but it can also resolve immediately if
|
|
the data already exists.
|
|
"""
|
|
|
|
_data: Optional[T] = None
|
|
_future: Optional[Future] = None
|
|
on_cancel: Optional[Callable[[], None]] = None
|
|
|
|
def __init__(self, data_resolver: Union[T, Callable[[], T]]):
|
|
if callable(data_resolver):
|
|
|
|
def future_complete(f: Future):
|
|
self._data = f.result()
|
|
|
|
self._future = AdapterManager.executor.submit(data_resolver)
|
|
self._future.add_done_callback(future_complete)
|
|
else:
|
|
self._data = data_resolver
|
|
|
|
def result(self) -> T:
|
|
if self._data is not None:
|
|
return self._data
|
|
if self._future is not None:
|
|
return self._future.result()
|
|
|
|
raise Exception("AdapterManager.Result had neither _data nor _future member!")
|
|
|
|
def add_done_callback(self, fn: Callable, *args):
|
|
if self._future is not None:
|
|
self._future.add_done_callback(fn, *args)
|
|
else:
|
|
# Run the function immediately if it's not a future.
|
|
fn(self, *args)
|
|
|
|
def cancel(self) -> bool:
|
|
if self._future is not None:
|
|
return self._future.cancel()
|
|
return True
|
|
|
|
@property
|
|
def data_is_available(self) -> bool:
|
|
return self._data is not None
|
|
|
|
|
|
class AdapterManager:
|
|
available_adapters: Set[Any] = {FilesystemAdapter, SubsonicAdapter}
|
|
executor: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=50)
|
|
is_shutting_down: bool = False
|
|
|
|
@dataclass
|
|
class _AdapterManagerInternal:
|
|
ground_truth_adapter: Adapter
|
|
caching_adapter: Optional[CachingAdapter] = None
|
|
|
|
def shutdown(self):
|
|
self.ground_truth_adapter.shutdown()
|
|
if self.caching_adapter:
|
|
self.caching_adapter.shutdown()
|
|
|
|
_instance: Optional[_AdapterManagerInternal] = None
|
|
|
|
@staticmethod
|
|
def register_adapter(adapter_class: Type):
|
|
if not issubclass(adapter_class, Adapter):
|
|
raise TypeError("Attempting to register a class that is not an adapter.")
|
|
AdapterManager.available_adapters.add(adapter_class)
|
|
|
|
def __init__(self):
|
|
"""
|
|
This should not ever be called. You should only ever use the static methods on
|
|
this class.
|
|
"""
|
|
raise Exception(
|
|
"Do not instantiate the AdapterManager. "
|
|
"Only use the static methods on the class."
|
|
)
|
|
|
|
@staticmethod
|
|
def shutdown():
|
|
logging.info("AdapterManager shutdown start")
|
|
AdapterManager.is_shutting_down = True
|
|
AdapterManager.executor.shutdown()
|
|
if AdapterManager._instance:
|
|
AdapterManager._instance.shutdown()
|
|
|
|
logging.info("CacheManager shutdown complete")
|
|
|
|
@staticmethod
|
|
def reset(config: AppConfiguration):
|
|
# First, shutdown the current one...
|
|
if AdapterManager._instance:
|
|
AdapterManager._instance.shutdown()
|
|
|
|
# TODO: actually do stuff with the config to determine which adapters
|
|
# to create, etc.
|
|
assert config.server is not None
|
|
source_data_dir = Path(config.cache_location, config.server.strhash())
|
|
source_data_dir.joinpath("g").mkdir(parents=True, exist_ok=True)
|
|
source_data_dir.joinpath("c").mkdir(parents=True, exist_ok=True)
|
|
|
|
ground_truth_adapter_type = SubsonicAdapter
|
|
ground_truth_adapter = ground_truth_adapter_type(
|
|
{
|
|
key: getattr(config.server, key)
|
|
for key in ground_truth_adapter_type.get_config_parameters()
|
|
},
|
|
source_data_dir.joinpath("g"),
|
|
)
|
|
|
|
caching_adapter_type = FilesystemAdapter
|
|
caching_adapter = None
|
|
if caching_adapter_type and ground_truth_adapter_type.can_be_cached:
|
|
caching_adapter = caching_adapter_type(
|
|
{
|
|
key: getattr(config.server, key)
|
|
for key in caching_adapter_type.get_config_parameters()
|
|
},
|
|
source_data_dir.joinpath("c"),
|
|
is_cache=True,
|
|
)
|
|
|
|
AdapterManager._instance = AdapterManager._AdapterManagerInternal(
|
|
ground_truth_adapter, caching_adapter=caching_adapter,
|
|
)
|
|
|
|
@staticmethod
|
|
def can_get_playlists() -> bool:
|
|
# It only matters that the ground truth one can service the request.
|
|
return (
|
|
AdapterManager._instance is not None
|
|
and AdapterManager._instance.ground_truth_adapter.can_service_requests
|
|
and AdapterManager._instance.ground_truth_adapter.can_get_playlists
|
|
)
|
|
|
|
@staticmethod
|
|
def get_playlists(
|
|
before_download: Callable[[], None] = lambda: None,
|
|
force: bool = False, # TODO: rename to use_ground_truth_adapter?
|
|
) -> Result[Sequence[Playlist]]:
|
|
assert AdapterManager._instance
|
|
if (
|
|
not force
|
|
and AdapterManager._instance.caching_adapter
|
|
and AdapterManager._instance.caching_adapter.can_service_requests
|
|
and AdapterManager._instance.caching_adapter.can_get_playlists
|
|
):
|
|
try:
|
|
return Result(AdapterManager._instance.caching_adapter.get_playlists())
|
|
except CacheMissError:
|
|
logging.debug(f'Cache Miss on {"get_playlists"}.')
|
|
except Exception:
|
|
logging.exception(f'Error on {"get_playlists"} retrieving from cache.')
|
|
|
|
if (
|
|
AdapterManager._instance.ground_truth_adapter
|
|
and not AdapterManager._instance.ground_truth_adapter.can_service_requests
|
|
and not AdapterManager._instance.ground_truth_adapter.can_get_playlists
|
|
):
|
|
raise Exception(f'No adapters can service {"get_playlists"} at the moment.')
|
|
|
|
def future_fn() -> Sequence[Playlist]:
|
|
assert AdapterManager._instance
|
|
if before_download:
|
|
before_download()
|
|
return AdapterManager._instance.ground_truth_adapter.get_playlists()
|
|
|
|
future: Result[Sequence[Playlist]] = Result(future_fn)
|
|
|
|
if AdapterManager._instance.caching_adapter:
|
|
|
|
def future_finished(f: Future):
|
|
assert AdapterManager._instance
|
|
assert AdapterManager._instance.caching_adapter
|
|
AdapterManager._instance.caching_adapter.ingest_new_data(
|
|
CachingAdapter.FunctionNames.GET_PLAYLISTS, (), f.result(),
|
|
)
|
|
|
|
future.add_done_callback(future_finished)
|
|
|
|
return future
|
|
|
|
@staticmethod
|
|
def can_get_playlist_details() -> bool:
|
|
# It only matters that the ground truth one can service the request.
|
|
return (
|
|
AdapterManager._instance is not None
|
|
and AdapterManager._instance.ground_truth_adapter.can_service_requests
|
|
and AdapterManager._instance.ground_truth_adapter.can_get_playlist_details
|
|
)
|
|
|
|
@staticmethod
|
|
def get_playlist_details(
|
|
playlist_id: str,
|
|
before_download: Callable[[], None] = lambda: None,
|
|
force: bool = False, # TODO: rename to use_ground_truth_adapter?
|
|
) -> Result[PlaylistDetails]:
|
|
assert AdapterManager._instance
|
|
partial_playlist_data = None
|
|
if (
|
|
not force
|
|
and AdapterManager._instance.caching_adapter
|
|
and AdapterManager._instance.caching_adapter.can_service_requests
|
|
and AdapterManager._instance.caching_adapter.can_get_playlist_details
|
|
):
|
|
try:
|
|
return Result(
|
|
AdapterManager._instance.caching_adapter.get_playlist_details(
|
|
playlist_id
|
|
)
|
|
)
|
|
except CacheMissError as e:
|
|
partial_playlist_data = e.partial_data
|
|
logging.debug(f'Cache Miss on {"get_playlist_details"}.')
|
|
except Exception:
|
|
logging.exception(
|
|
f'Error on {"get_playlist_details"} retrieving from cache.'
|
|
)
|
|
|
|
if (
|
|
AdapterManager._instance.ground_truth_adapter
|
|
and not AdapterManager._instance.ground_truth_adapter.can_service_requests
|
|
and not (
|
|
AdapterManager._instance.ground_truth_adapter.can_get_playlist_details
|
|
)
|
|
):
|
|
if partial_playlist_data:
|
|
# TODO do something here
|
|
pass
|
|
raise Exception(
|
|
f'No adapters can service {"get_playlist_details"} at the moment.'
|
|
)
|
|
|
|
def future_fn() -> PlaylistDetails:
|
|
assert AdapterManager._instance
|
|
if before_download:
|
|
before_download()
|
|
return AdapterManager._instance.ground_truth_adapter.get_playlist_details(
|
|
playlist_id
|
|
)
|
|
|
|
future: Result[PlaylistDetails] = Result(future_fn)
|
|
|
|
if AdapterManager._instance.caching_adapter:
|
|
|
|
def future_finished(f: Future):
|
|
assert AdapterManager._instance
|
|
assert AdapterManager._instance.caching_adapter
|
|
AdapterManager._instance.caching_adapter.ingest_new_data(
|
|
CachingAdapter.FunctionNames.GET_PLAYLIST_DETAILS,
|
|
(playlist_id,),
|
|
f.result(),
|
|
)
|
|
|
|
future.add_done_callback(future_finished)
|
|
|
|
return future
|