Fixed a couple of bugs
This commit is contained in:
@@ -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:
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
|
@@ -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__"]
|
||||
|
||||
|
@@ -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, ...]):
|
||||
|
Reference in New Issue
Block a user