Fixed a couple of bugs

This commit is contained in:
Sumner Evans
2020-05-29 18:01:58 -06:00
parent ee8fd84782
commit 32b6f98eb0
5 changed files with 124 additions and 91 deletions

View File

@@ -117,9 +117,9 @@ class AlbumSearchQuery:
"""
if not self._strhash:
hash_tuple: Tuple[Any, ...] = (self.type.value,)
if self.type.value == AlbumSearchQuery.Type.YEAR_RANGE:
if self.type == AlbumSearchQuery.Type.YEAR_RANGE:
hash_tuple += (self.year_range,)
elif self.type.value == AlbumSearchQuery.Type.GENRE:
elif self.type == AlbumSearchQuery.Type.GENRE:
hash_tuple += (self.genre.name,)
self._strhash = hashlib.sha1(bytes(str(hash_tuple), "utf8")).hexdigest()
return self._strhash
@@ -299,6 +299,12 @@ class Adapter(abc.ABC):
"""
return True
def on_offline_mode_change(self, offline_mode: bool):
"""
This function should be used to handle any operations that need to be performed
when Sublime Music goes from online to offline mode or vice versa.
"""
@property
@abc.abstractmethod
def ping_status(self) -> bool:

View File

@@ -166,7 +166,7 @@ class AdapterManager:
executor: ThreadPoolExecutor = ThreadPoolExecutor()
download_executor: ThreadPoolExecutor = ThreadPoolExecutor()
is_shutting_down: bool = False
offline_mode: bool = False
_offline_mode: bool = False
@dataclass
class _AdapterManagerInternal:
@@ -241,7 +241,7 @@ class AdapterManager:
if AdapterManager._instance:
AdapterManager._instance.shutdown()
AdapterManager.offline_mode = config.offline_mode
AdapterManager._offline_mode = config.offline_mode
# TODO (#197): actually do stuff with the config to determine which adapters to
# create, etc.
@@ -277,6 +277,14 @@ class AdapterManager:
concurrent_download_limit=config.concurrent_download_limit,
)
@staticmethod
def on_offline_mode_change(offline_mode: bool):
AdapterManager._offline_mode = offline_mode
if (instance := AdapterManager._instance) and (
(ground_truth_adapter := instance.ground_truth_adapter).is_networked
):
ground_truth_adapter.on_offline_mode_change(offline_mode)
# Data Helper Methods
# ==================================================================================
TAdapter = TypeVar("TAdapter", bound=Adapter)
@@ -289,11 +297,9 @@ class AdapterManager:
def _ground_truth_can_do(action_name: str) -> bool:
if not AdapterManager._instance:
return False
ground_truth_adapter = AdapterManager._instance.ground_truth_adapter
if AdapterManager.offline_mode and ground_truth_adapter.is_networked:
return False
return AdapterManager._adapter_can_do(ground_truth_adapter, action_name)
return AdapterManager._adapter_can_do(
AdapterManager._instance.ground_truth_adapter, action_name
)
@staticmethod
def _can_use_cache(force: bool, action_name: str) -> bool:
@@ -318,17 +324,15 @@ class AdapterManager:
"""
Creates a Result using the given ``function_name`` on the ground truth adapter.
"""
if (
AdapterManager.offline_mode
and AdapterManager._instance
and AdapterManager._instance.ground_truth_adapter.is_networked
):
raise AssertionError(
"You should never call _create_ground_truth_result in offline mode"
)
def future_fn() -> Any:
assert AdapterManager._instance
if (
AdapterManager._offline_mode
and AdapterManager._instance.ground_truth_adapter.is_networked
):
raise CacheMissError(partial_data=partial_data)
if before_download:
before_download()
fn = getattr(AdapterManager._instance.ground_truth_adapter, function_name)
@@ -348,14 +352,6 @@ class AdapterManager:
filename. The returned function will spin-loop if the resource is already being
downloaded to prevent multiple requests for the same download.
"""
if (
AdapterManager.offline_mode
and AdapterManager._instance
and AdapterManager._instance.ground_truth_adapter.is_networked
):
raise AssertionError(
"You should never call _create_download_fn in offline mode"
)
def download_fn() -> str:
assert AdapterManager._instance
@@ -699,9 +695,9 @@ class AdapterManager:
def delete_playlist(playlist_id: str):
assert AdapterManager._instance
ground_truth_adapter = AdapterManager._instance.ground_truth_adapter
if AdapterManager.offline_mode and ground_truth_adapter.is_networked:
if AdapterManager._offline_mode and ground_truth_adapter.is_networked:
raise AssertionError(
"You should never call _create_download_fn in offline mode"
"You should never call delete_playlist in offline mode"
)
# TODO (#190): make non-blocking?
@@ -742,6 +738,10 @@ class AdapterManager:
assert AdapterManager._instance
# If the ground truth adapter can't provide cover art, just give up immediately.
if not AdapterManager._ground_truth_can_do("get_cover_art_uri"):
return Result(existing_cover_art_filename)
# There could be partial data if the cover art exists, but for some reason was
# marked out-of-date.
if AdapterManager._can_use_cache(force, "get_cover_art_uri"):
@@ -761,15 +761,15 @@ class AdapterManager:
f'Error on {"get_cover_art_uri"} retrieving from cache.'
)
if not allow_download:
return Result(existing_cover_art_filename)
if AdapterManager._instance.caching_adapter and force:
AdapterManager._instance.caching_adapter.invalidate_data(
CachingAdapter.CachedDataKey.COVER_ART_FILE, cover_art_id
)
if not AdapterManager._ground_truth_can_do("get_cover_art_uri"):
if not allow_download or (
AdapterManager._offline_mode
and AdapterManager._instance.ground_truth_adapter.is_networked
):
return Result(existing_cover_art_filename)
future: Result[str] = Result(
@@ -848,7 +848,7 @@ class AdapterManager:
) -> Result[None]:
assert AdapterManager._instance
if (
AdapterManager.offline_mode
AdapterManager._offline_mode
and AdapterManager._instance.ground_truth_adapter.is_networked
):
raise AssertionError(
@@ -883,6 +883,7 @@ class AdapterManager:
before_download(song_id)
# Download the song.
# TODO (#64) handle download errors?
song_tmp_filename = AdapterManager._create_download_fn(
AdapterManager._instance.ground_truth_adapter.get_song_uri(
song_id, AdapterManager._get_scheme()

View File

@@ -126,48 +126,58 @@ class SubsonicAdapter(Adapter):
self.disable_cert_verify = config.get("disable_cert_verify")
self.is_shutting_down = False
self.ping_process = multiprocessing.Process(target=self._check_ping_thread)
self.ping_process.start()
# TODO (#112): support XML?
_ping_process: Optional[multiprocessing.Process] = None
_offline_mode = False
def initial_sync(self):
# Wait for a server ping to happen.
tries = 0
while not self._server_available.value and tries < 5:
sleep(0.1)
self._set_ping_status()
tries += 1
# Try to ping the server five times using exponential backoff (2^5 = 32s).
self._exponential_backoff(5)
def shutdown(self):
self.ping_process.terminate()
if self._ping_process:
self._ping_process.terminate()
# Availability Properties
# ==================================================================================
_server_available = multiprocessing.Value("b", False)
_last_ping_timestamp = multiprocessing.Value("d", 0.0)
def _check_ping_thread(self):
# TODO (#96): also use NM to detect when the connection changes and update
# accordingly.
def _exponential_backoff(self, n: int):
logging.info(f"Starting Exponential Backoff: n={n}")
if self._ping_process:
self._ping_process.terminate()
# TODO don't ping in offline mode
while True:
self._set_ping_status()
sleep(15)
self._ping_process = multiprocessing.Process(
target=self._check_ping_thread, args=(n,)
)
self._ping_process.start()
def _set_ping_status(self):
def _check_ping_thread(self, n: int):
i = 0
while i < n and not self._offline_mode and not self._server_available.value:
try:
self._set_ping_status(timeout=2 * (i + 1))
except Exception:
pass
sleep(2 ** i)
i += 1
def _set_ping_status(self, timeout: int = 2):
logging.info(f"SET PING STATUS timeout={timeout}")
now = datetime.now().timestamp()
if now - self._last_ping_timestamp.value < 15:
return
try:
# Try to ping the server with a timeout of 2 seconds.
self._get_json(self._make_url("ping"), timeout=2)
except Exception:
logging.exception(f"Could not connect to {self.hostname}")
self._server_available.value = False
self._last_ping_timestamp.value = now
# Try to ping the server.
self._get_json(
self._make_url("ping"), timeout=timeout, is_exponential_backoff_ping=True,
)
def on_offline_mode_change(self, offline_mode: bool):
self._offline_mode = offline_mode
@property
def ping_status(self) -> bool:
@@ -241,46 +251,59 @@ class SubsonicAdapter(Adapter):
url: str,
timeout: Union[float, Tuple[float, float], None] = None,
# TODO (#122): retry count
is_exponential_backoff_ping: bool = False,
**params,
) -> Any:
params = {**self._get_params(), **params}
logging.info(f"[START] get: {url}")
if REQUEST_DELAY is not None:
delay = random.uniform(*REQUEST_DELAY)
logging.info(f"REQUEST_DELAY enabled. Pausing for {delay} seconds")
sleep(delay)
if timeout:
if type(timeout) == tuple:
if delay > cast(Tuple[float, float], timeout)[0]:
raise TimeoutError("DUMMY TIMEOUT ERROR")
else:
if delay > cast(float, timeout):
raise TimeoutError("DUMMY TIMEOUT ERROR")
try:
if REQUEST_DELAY is not None:
delay = random.uniform(*REQUEST_DELAY)
logging.info(f"REQUEST_DELAY enabled. Pausing for {delay} seconds")
sleep(delay)
if timeout:
if type(timeout) == tuple:
if delay > cast(Tuple[float, float], timeout)[0]:
raise TimeoutError("DUMMY TIMEOUT ERROR")
else:
if delay > cast(float, timeout):
raise TimeoutError("DUMMY TIMEOUT ERROR")
if NETWORK_ALWAYS_ERROR:
raise Exception("NETWORK_ALWAYS_ERROR enabled")
if NETWORK_ALWAYS_ERROR:
raise Exception("NETWORK_ALWAYS_ERROR enabled")
# Deal with datetime parameters (convert to milliseconds since 1970)
for k, v in params.items():
if isinstance(v, datetime):
params[k] = int(v.timestamp() * 1000)
# Deal with datetime parameters (convert to milliseconds since 1970)
for k, v in params.items():
if isinstance(v, datetime):
params[k] = int(v.timestamp() * 1000)
if self._is_mock:
logging.info("Using mock data")
result = self._get_mock_data()
else:
result = requests.get(
url, params=params, verify=not self.disable_cert_verify, timeout=timeout
)
if self._is_mock:
logging.info("Using mock data")
result = self._get_mock_data()
else:
result = requests.get(
url,
params=params,
verify=not self.disable_cert_verify,
timeout=timeout,
)
# TODO (#122): make better
if result.status_code != 200:
raise Exception(f"[FAIL] get: {url} status={result.status_code}")
# TODO (#122): make better
if result.status_code != 200:
raise Exception(f"[FAIL] get: {url} status={result.status_code}")
# Any time that a server request succeeds, then we win.
self._server_available.value = True
self._last_ping_timestamp.value = datetime.now().timestamp()
# Any time that a server request succeeds, then we win.
self._server_available.value = True
self._last_ping_timestamp.value = datetime.now().timestamp()
except Exception:
logging.exception(f"get: {url} failed")
self._server_available.value = False
self._last_ping_timestamp.value = datetime.now().timestamp()
if not is_exponential_backoff_ping:
self._exponential_backoff(5)
raise
logging.info(f"[FINISH] get: {url}")
return result
@@ -289,6 +312,7 @@ class SubsonicAdapter(Adapter):
self,
url: str,
timeout: Union[float, Tuple[float, float], None] = None,
is_exponential_backoff_ping: bool = False,
**params: Union[None, str, datetime, int, Sequence[int], Sequence[str]],
) -> Response:
"""
@@ -298,7 +322,12 @@ class SubsonicAdapter(Adapter):
:returns: a dictionary of the subsonic response.
:raises Exception: needs some work
"""
result = self._get(url, timeout=timeout, **params)
result = self._get(
url,
timeout=timeout,
is_exponential_backoff_ping=is_exponential_backoff_ping,
**params,
)
subsonic_response = result.json().get("subsonic-response")
# TODO (#122): make better

View File

@@ -515,7 +515,7 @@ class SublimeMusicApp(Gtk.Application):
for k, v in settings.items():
setattr(self.app_config, k, v)
if (offline_mode := settings.get("offline_mode")) is not None:
AdapterManager.offline_mode = offline_mode
AdapterManager.on_offline_mode_change(offline_mode)
del state_updates["__settings__"]

View File

@@ -56,9 +56,6 @@ class BrowsePanel(Gtk.Overlay):
self.add_overlay(self.spinner)
def update(self, app_config: AppConfiguration, force: bool = False):
if not AdapterManager.can_get_directory():
return
self.update_order_token += 1
def do_update(update_order_token: int, id_stack: Tuple[str, ...]):