Use a deterministic hash
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
|
import hashlib
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from base64 import b64encode
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -54,6 +57,11 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
can_service_requests: bool = True
|
can_service_requests: bool = True
|
||||||
|
|
||||||
|
# Data Helper Methods
|
||||||
|
# =========================================================================
|
||||||
|
def _params_hash(self, *params: Any) -> str:
|
||||||
|
return hashlib.sha1(bytes(json.dumps(params), 'utf8')).hexdigest()
|
||||||
|
|
||||||
# Data Retrieval Methods
|
# Data Retrieval Methods
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
can_get_playlists: bool = True
|
can_get_playlists: bool = True
|
||||||
@@ -87,7 +95,8 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
function_name = CachingAdapter.FunctionNames.GET_PLAYLIST_DETAILS
|
function_name = CachingAdapter.FunctionNames.GET_PLAYLIST_DETAILS
|
||||||
cache_info = models.CacheInfo.get_or_none(
|
cache_info = models.CacheInfo.get_or_none(
|
||||||
models.CacheInfo.query_name == function_name,
|
models.CacheInfo.query_name == function_name,
|
||||||
params_hash=hash((playlist_id, ), ))
|
params_hash=self._params_hash(playlist_id),
|
||||||
|
)
|
||||||
if not cache_info:
|
if not cache_info:
|
||||||
raise CacheMissError(partial_data=playlist)
|
raise CacheMissError(partial_data=playlist)
|
||||||
|
|
||||||
@@ -106,7 +115,7 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
|
|
||||||
models.CacheInfo.insert(
|
models.CacheInfo.insert(
|
||||||
query_name=function,
|
query_name=function,
|
||||||
params_hash=hash(params),
|
params_hash=self._params_hash(*params),
|
||||||
last_ingestion_time=datetime.now(),
|
last_ingestion_time=datetime.now(),
|
||||||
).on_conflict_replace().execute()
|
).on_conflict_replace().execute()
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ from typing import Any, Optional, Sequence
|
|||||||
|
|
||||||
from peewee import (
|
from peewee import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
|
CompositeKey,
|
||||||
DoubleField,
|
DoubleField,
|
||||||
ensure_tuple,
|
ensure_tuple,
|
||||||
ForeignKeyField,
|
ForeignKeyField,
|
||||||
@@ -23,14 +24,6 @@ database = SqliteDatabase(None)
|
|||||||
|
|
||||||
# Custom Fields
|
# Custom Fields
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
class DurationField(DoubleField):
|
|
||||||
def db_value(self, value: timedelta) -> Optional[float]:
|
|
||||||
return value.total_seconds() if value else None
|
|
||||||
|
|
||||||
def python_value(self, value: Optional[float]) -> Optional[timedelta]:
|
|
||||||
return timedelta(seconds=value) if value else None
|
|
||||||
|
|
||||||
|
|
||||||
class CacheConstantsField(TextField):
|
class CacheConstantsField(TextField):
|
||||||
def db_value(self, value: CachingAdapter.FunctionNames) -> str:
|
def db_value(self, value: CachingAdapter.FunctionNames) -> str:
|
||||||
return value.value
|
return value.value
|
||||||
@@ -39,6 +32,14 @@ class CacheConstantsField(TextField):
|
|||||||
return CachingAdapter.FunctionNames(value)
|
return CachingAdapter.FunctionNames(value)
|
||||||
|
|
||||||
|
|
||||||
|
class DurationField(DoubleField):
|
||||||
|
def db_value(self, value: timedelta) -> Optional[float]:
|
||||||
|
return value.total_seconds() if value else None
|
||||||
|
|
||||||
|
def python_value(self, value: Optional[float]) -> Optional[timedelta]:
|
||||||
|
return timedelta(seconds=value) if value else None
|
||||||
|
|
||||||
|
|
||||||
class TzDateTimeField(TextField):
|
class TzDateTimeField(TextField):
|
||||||
def db_value(self, value: Optional[datetime]) -> Optional[str]:
|
def db_value(self, value: Optional[datetime]) -> Optional[str]:
|
||||||
return value.isoformat() if value else None
|
return value.isoformat() if value else None
|
||||||
@@ -204,10 +205,13 @@ class Song(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CacheInfo(BaseModel):
|
class CacheInfo(BaseModel):
|
||||||
query_name = CacheConstantsField(unique=True, primary_key=True)
|
query_name = CacheConstantsField()
|
||||||
params_hash = IntegerField(null=False)
|
params_hash = TextField()
|
||||||
last_ingestion_time = TzDateTimeField(null=False)
|
last_ingestion_time = TzDateTimeField(null=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
primary_key = CompositeKey('query_name', 'params_hash')
|
||||||
|
|
||||||
|
|
||||||
class Playlist(BaseModel):
|
class Playlist(BaseModel):
|
||||||
id = TextField(unique=True, primary_key=True)
|
id = TextField(unique=True, primary_key=True)
|
||||||
|
@@ -134,6 +134,9 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
self.window.show_all()
|
self.window.show_all()
|
||||||
self.window.present()
|
self.window.present()
|
||||||
|
|
||||||
|
# Load the state for the server, if it exists.
|
||||||
|
self.app_config.load_state()
|
||||||
|
|
||||||
# If there is no current server, show the dialog to select a server.
|
# If there is no current server, show the dialog to select a server.
|
||||||
if self.app_config.server is None:
|
if self.app_config.server is None:
|
||||||
self.show_configure_servers_dialog()
|
self.show_configure_servers_dialog()
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
from dataclasses import asdict, dataclass, field, fields
|
from dataclasses import asdict, dataclass, field, fields
|
||||||
@@ -68,7 +69,7 @@ class ServerConfiguration:
|
|||||||
>>> sc.strhash()
|
>>> sc.strhash()
|
||||||
'6df23dc03f9b54cc38a0fc1483df6e21'
|
'6df23dc03f9b54cc38a0fc1483df6e21'
|
||||||
"""
|
"""
|
||||||
server_info = (self.name + self.server_address + self.username)
|
server_info = self.name + self.server_address + self.username
|
||||||
return hashlib.md5(server_info.encode('utf-8')).hexdigest()
|
return hashlib.md5(server_info.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ class AppConfiguration:
|
|||||||
|
|
||||||
config = AppConfiguration(**args)
|
config = AppConfiguration(**args)
|
||||||
config.filename = filename
|
config.filename = filename
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
@@ -136,16 +138,25 @@ class AppConfiguration:
|
|||||||
if not server:
|
if not server:
|
||||||
return UIState()
|
return UIState()
|
||||||
|
|
||||||
# If already retrieved, and the server hasn't changed, then return the
|
# If the server has changed, then retrieve the new server's state.
|
||||||
# state. Don't use strhash because that is much more expensive of an
|
# TODO: if things are slow, then use a different hash
|
||||||
# operation.
|
if self._current_server_hash != server.strhash():
|
||||||
if self._current_server_hash != hash(server) or not self._state:
|
self.load_state()
|
||||||
self._current_server_hash = hash(server)
|
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def load_state(self):
|
||||||
|
if not self.server:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._current_server_hash = self.server.strhash()
|
||||||
if self.state_file_location.exists():
|
if self.state_file_location.exists():
|
||||||
try:
|
try:
|
||||||
with open(self.state_file_location, 'rb') as f:
|
with open(self.state_file_location, 'rb') as f:
|
||||||
self._state = UIState(**pickle.load(f))
|
self._state = UIState(**pickle.load(f))
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logging.warning(
|
||||||
|
f"Couldn't load state from {self.state_file_location}")
|
||||||
# Just ignore any errors, it is only UI state.
|
# Just ignore any errors, it is only UI state.
|
||||||
self._state = UIState()
|
self._state = UIState()
|
||||||
|
|
||||||
@@ -155,8 +166,6 @@ class AppConfiguration:
|
|||||||
CacheManager.reset(self)
|
CacheManager.reset(self)
|
||||||
AdapterManager.reset(self)
|
AdapterManager.reset(self)
|
||||||
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_file_location(self) -> Path:
|
def state_file_location(self) -> Path:
|
||||||
assert self.server is not None
|
assert self.server is not None
|
||||||
|
@@ -178,7 +178,7 @@ def test_caching_get_playlist_then_details(cache_adapter: FilesystemAdapter):
|
|||||||
FilesystemAdapter.FunctionNames.GET_PLAYLISTS,
|
FilesystemAdapter.FunctionNames.GET_PLAYLISTS,
|
||||||
(),
|
(),
|
||||||
[
|
[
|
||||||
SubsonicAPI.Playlist('1', 'test1', comment='comment'),
|
SubsonicAPI.Playlist('1', 'test1'),
|
||||||
SubsonicAPI.Playlist('2', 'test2'),
|
SubsonicAPI.Playlist('2', 'test2'),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -192,3 +192,33 @@ def test_caching_get_playlist_then_details(cache_adapter: FilesystemAdapter):
|
|||||||
assert e.partial_data
|
assert e.partial_data
|
||||||
assert e.partial_data.id == '1'
|
assert e.partial_data.id == '1'
|
||||||
assert e.partial_data.name == 'test1'
|
assert e.partial_data.name == 'test1'
|
||||||
|
|
||||||
|
# Simulate getting playlist details for id=1, then id=2
|
||||||
|
songs = [
|
||||||
|
SubsonicAPI.Song(
|
||||||
|
'3',
|
||||||
|
'Song 3',
|
||||||
|
parent='foo',
|
||||||
|
album='foo',
|
||||||
|
artist='foo',
|
||||||
|
duration=timedelta(seconds=10.2),
|
||||||
|
path='/foo/song3.mp3',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
cache_adapter.ingest_new_data(
|
||||||
|
FilesystemAdapter.FunctionNames.GET_PLAYLIST_DETAILS,
|
||||||
|
('1', ),
|
||||||
|
SubsonicAPI.PlaylistWithSongs('1', 'test1', songs=songs),
|
||||||
|
)
|
||||||
|
|
||||||
|
cache_adapter.ingest_new_data(
|
||||||
|
FilesystemAdapter.FunctionNames.GET_PLAYLIST_DETAILS,
|
||||||
|
('2', ),
|
||||||
|
SubsonicAPI.PlaylistWithSongs('2', 'test2', songs=songs),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Going back and getting playlist details for the first one should not
|
||||||
|
# cache miss.
|
||||||
|
playlist = cache_adapter.get_playlist_details('1')
|
||||||
|
assert playlist.id == '1'
|
||||||
|
assert playlist.name == 'test1'
|
||||||
|
Reference in New Issue
Block a user