A bunch of cleanup

This commit is contained in:
Sumner Evans
2020-05-15 09:05:57 -06:00
parent 8017aac704
commit 1b980534f6
18 changed files with 94 additions and 340 deletions

116
Pipfile.lock generated
View File

@@ -113,13 +113,6 @@
],
"version": "==4.3.2"
},
"deprecated": {
"hashes": [
"sha256:0cf37d293a96805c6afd8b5fc525cb40f23a2cac9b2d066ac3bd4b04e72ceccc",
"sha256:55b41a15bda04c6a2c0d27dd4c2b7b81ffa6348c9cad8f077ac1978c59927ab9"
],
"version": "==1.2.9"
},
"fuzzywuzzy": {
"hashes": [
"sha256:45016e92264780e58972dca1b3d939ac864b78437422beecebb3095f8efd00e8",
@@ -190,24 +183,24 @@
},
"protobuf": {
"hashes": [
"sha256:03b31ec00ad94d4947fd87f49b288e60f443370fd1927fae80411d2dd864fbb5",
"sha256:0b845c1fb8f36be203cd2ca9e405a22ee2cec2ed87d180b067d7c063f5701633",
"sha256:0d1fec40323c8e10812897c71453c33401f6ccc6ade98c5a3fef1f019de797e6",
"sha256:375ab5683efc946d1340dcf53dd422ccb55fbe88c0e16408182ca9a73248d91e",
"sha256:3c5a1a0acd42a3fa39ce0b1436cd7faaa1e77ecaac58cd87101f56b2fe99f628",
"sha256:3f01f6a479aff857615f2decaba773470816727fa6be6291866bd966d6ae3c61",
"sha256:4cae6edd604ddbbaadd90da13df06fdf399d3fa9f19950e78340ab69f59f103c",
"sha256:4edae95bff0e4a010059462b4a0116366863573c105ba689fc19ed9dae16888d",
"sha256:67412c3eb0299a2c908d86dea1ceab9e65558684abd2f53e9f85ae28f03ba7b3",
"sha256:8765978e2e553a7a9a7d4aa64b957f111a0358d85d799e378dc458b653ea2de5",
"sha256:8bba760eb61044120cb91552f55c4b2fa3a80c8639fae8583b53b3e3a7e8da56",
"sha256:996542402404aa8577defcdebbf9a0780bd96c7af2f562eefd4542716ca369a1",
"sha256:a4cb8388c3f75d36ac51667e678f4c3096f672229d3e68d1db18675d4f59e5a2",
"sha256:b8404d27772f130299185e20e4379a2b3450c7d1197396131cc2ec4626db75cb",
"sha256:b9c9692d2842ff7846b0c2574be8e921247b7c377f4c03cd6370aef077fb652c",
"sha256:caed753a89e5ffc2fbbf624926eacc3924c884181374bd3ddf54ca0a2903eb11"
"sha256:09e29cc89b57741ae04bbf219ec723d08544d7b908f460fc3864dc3d7e22e903",
"sha256:1ca56aa79c774af7a50934d4f75006d278d6399a3120d804827e2fc33a56ce97",
"sha256:1f5a80dfcd805b06ebebd81c3d691ff01db8b98172c71c41d1a3ab0e7907bff4",
"sha256:206d1f61a092d308b367b331ab216c94328ba820e63f811fafade548e293feb8",
"sha256:46736b7774685ad84fe4eb730d2496b925b8d6a880781ba988247119162a5278",
"sha256:4b8886683e9a8fec0078793db58faf73e4d99704c2323780e1374e9e100a8111",
"sha256:4ce1f4364b793a1ccdd038910379be6b3c1791ce39fc921620ac96173a9f5ae3",
"sha256:6704d751386c15f010c991937b7b69cdce370e7a124e28451bdc3a217b4ad2e9",
"sha256:67a41c93b016f47d404dd12304bb357654959e4b13078ecaf1ad22c2c299b3ed",
"sha256:71e6e0dc6a1ae4daaf3b172615e0543e7b0dc2376b5c18251daf6dfc10f50676",
"sha256:78470a2463c0499f9253a98d74c74ec0c440c276e9090f65c21480e1a5408d33",
"sha256:bded9b237935d7e6275773b576ddbddd655f9e676a05c1ac0b24e013083adf66",
"sha256:c980e4dcb982e37543a05fb8609029858268435e1568cb8893146f533510b229",
"sha256:ddcddc29ba29099a548bb49dbd87fc6b049dd1dd031b3154efc4df1963a5df69",
"sha256:ea525877ac33be8a1f6441484702d6416b731c7053bfb237ab006238584e5db4",
"sha256:ef4f091a8b4256d8982135eeff189df18b56e5215be7cef07cf886d67daa92a9"
],
"version": "==3.12.0rc1"
"version": "==3.12.0rc2"
},
"pycairo": {
"hashes": [
@@ -217,10 +210,10 @@
},
"pychromecast": {
"hashes": [
"sha256:16f90297c0b8930a223ac35e7a6188f87301d9e3db64d662be1073ebae533e6b",
"sha256:9dadfc91d038e30abc5c909ebdec3e6c6f9d0d6765222557228a5c591025fc68"
"sha256:078e78dbf1ca596211c06a67b7d79ae0e3d07edaa57acd647b58a3554a9c504d",
"sha256:90bfc191b2aa6de3b6941cb3635ea295a4e5aebced17070550dc953d66115814"
],
"version": "==5.1.0"
"version": "==5.2.0"
},
"pycparser": {
"hashes": [
@@ -235,13 +228,6 @@
],
"version": "==3.36.1"
},
"python-dateutil": {
"hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"version": "==2.8.1"
},
"python-levenshtein": {
"hashes": [
"sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1"
@@ -328,12 +314,6 @@
],
"version": "==1.25.9"
},
"wrapt": {
"hashes": [
"sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
],
"version": "==1.12.1"
},
"zeroconf": {
"hashes": [
"sha256:51f25787c27cf7b903e6795e8763bccdaa71199f61b75af97f1bde036fa43b27",
@@ -446,11 +426,11 @@
},
"flake8": {
"hashes": [
"sha256:bcf5163890bb01f11f04f0f444f01004d0f9ad5fab10c51104f770acf532008f",
"sha256:e2f33066fb92ac0a3a30ea509f61e325f2110b2e84644333a3ff8e9e98a2beab"
"sha256:6c1193b0c3f853ef763969238f6c81e9e63ace9d024518edc020d5f1d6d93195",
"sha256:ea6623797bf9a52f4c9577d780da0bb17d65f870213f7b5bcc9fca82540c31d5"
],
"index": "pypi",
"version": "==3.8.0"
"version": "==3.8.1"
},
"flake8-annotations": {
"hashes": [
@@ -718,29 +698,29 @@
},
"regex": {
"hashes": [
"sha256:021a0ae4d2baeeb60a3014805a2096cb329bd6d9f30669b7ad0da51a9cb73349",
"sha256:04d6e948ef34d3eac133bedc0098364a9e635a7914f050edb61272d2ddae3608",
"sha256:099568b372bda492be09c4f291b398475587d49937c659824f891182df728cdf",
"sha256:0ff50843535593ee93acab662663cb2f52af8e31c3f525f630f1dc6156247938",
"sha256:1b17bf37c2aefc4cac8436971fe6ee52542ae4225cfc7762017f7e97a63ca998",
"sha256:1e2255ae938a36e9bd7db3b93618796d90c07e5f64dd6a6750c55f51f8b76918",
"sha256:2bc6a17a7fa8afd33c02d51b6f417fc271538990297167f68a98cae1c9e5c945",
"sha256:3ab5e41c4ed7cd4fa426c50add2892eb0f04ae4e73162155cd668257d02259dd",
"sha256:3b059e2476b327b9794c792c855aa05531a3f3044737e455d283c7539bd7534d",
"sha256:4df91094ced6f53e71f695c909d9bad1cca8761d96fd9f23db12245b5521136e",
"sha256:5493a02c1882d2acaaf17be81a3b65408ff541c922bfd002535c5f148aa29f74",
"sha256:5b741ecc3ad3e463d2ba32dce512b412c319993c1bb3d999be49e6092a769fb2",
"sha256:652ab4836cd5531d64a34403c00ada4077bb91112e8bcdae933e2eae232cf4a8",
"sha256:669a8d46764a09f198f2e91fc0d5acdac8e6b620376757a04682846ae28879c4",
"sha256:73a10404867b835f1b8a64253e4621908f0d71150eb4e97ab2e7e441b53e9451",
"sha256:7ce4a213a96d6c25eeae2f7d60d4dad89ac2b8134ec3e69db9bc522e2c0f9388",
"sha256:8127ca2bf9539d6a64d03686fd9e789e8c194fc19af49b69b081f8c7e6ecb1bc",
"sha256:b5b5b2e95f761a88d4c93691716ce01dc55f288a153face1654f868a8034f494",
"sha256:b7c9f65524ff06bf70c945cd8d8d1fd90853e27ccf86026af2afb4d9a63d06b1",
"sha256:f7f2f4226db6acd1da228adf433c5c3792858474e49d80668ea82ac87cf74a03",
"sha256:fa09da4af4e5b15c0e8b4986a083f3fd159302ea115a6cc0649cd163435538b8"
"sha256:1386e75c9d1574f6aa2e4eb5355374c8e55f9aac97e224a8a5a6abded0f9c927",
"sha256:27ff7325b297fb6e5ebb70d10437592433601c423f5acf86e5bc1ee2919b9561",
"sha256:329ba35d711e3428db6b45a53b1b13a0a8ba07cbbcf10bbed291a7da45f106c3",
"sha256:3a9394197664e35566242686d84dfd264c07b20f93514e2e09d3c2b3ffdf78fe",
"sha256:51f17abbe973c7673a61863516bdc9c0ef467407a940f39501e786a07406699c",
"sha256:579ea215c81d18da550b62ff97ee187b99f1b135fd894a13451e00986a080cad",
"sha256:70c14743320a68c5dac7fc5a0f685be63bc2024b062fe2aaccc4acc3d01b14a1",
"sha256:7e61be8a2900897803c293247ef87366d5df86bf701083b6c43119c7c6c99108",
"sha256:8044d1c085d49673aadb3d7dc20ef5cb5b030c7a4fa253a593dda2eab3059929",
"sha256:89d76ce33d3266173f5be80bd4efcbd5196cafc34100fdab814f9b228dee0fa4",
"sha256:99568f00f7bf820c620f01721485cad230f3fb28f57d8fbf4a7967ec2e446994",
"sha256:a7c37f048ec3920783abab99f8f4036561a174f1314302ccfa4e9ad31cb00eb4",
"sha256:c2062c7d470751b648f1cacc3f54460aebfc261285f14bc6da49c6943bd48bdd",
"sha256:c9bce6e006fbe771a02bda468ec40ffccbf954803b470a0345ad39c603402577",
"sha256:ce367d21f33e23a84fb83a641b3834dd7dd8e9318ad8ff677fbfae5915a239f7",
"sha256:ce450ffbfec93821ab1fea94779a8440e10cf63819be6e176eb1973a6017aff5",
"sha256:ce5cc53aa9fbbf6712e92c7cf268274eaff30f6bd12a0754e8133d85a8fb0f5f",
"sha256:d466967ac8e45244b9dfe302bbe5e3337f8dc4dec8d7d10f5e950d83b140d33a",
"sha256:d881c2e657c51d89f02ae4c21d9adbef76b8325fe4d5cf0e9ad62f850f3a98fd",
"sha256:e565569fc28e3ba3e475ec344d87ed3cd8ba2d575335359749298a0899fe122e",
"sha256:ea55b80eb0d1c3f1d8d784264a6764f931e172480a2f1868f2536444c5f01e01"
],
"version": "==2020.5.7"
"version": "==2020.5.14"
},
"requests": {
"hashes": [
@@ -844,10 +824,10 @@
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
],
"version": "==0.10.0"
"version": "==0.10.1"
},
"typed-ast": {
"hashes": [

View File

@@ -3,7 +3,6 @@ deepdiff==4.0.7
Deprecated==1.2.6
fuzzywuzzy==0.17.0
PyChromecast==3.2.3
python-dateutil==2.8.0
python-Levenshtein==0.12.0
python-mpv==0.3.9
PyYAML==5.1.2

View File

@@ -55,13 +55,11 @@ setup(
"bottle",
"dataclasses-json @ git+https://github.com/lidatong/dataclasses-json@master#egg=dataclasses-json", # noqa: E501
"deepdiff",
"Deprecated",
"fuzzywuzzy",
'osxmmkeys ; sys_platform=="darwin"',
"peewee",
"pychromecast",
"PyGObject",
"python-dateutil",
"python-Levenshtein",
"python-mpv",
"pyyaml",

View File

@@ -224,6 +224,7 @@ class Adapter(abc.ABC):
directory is guaranteed to exist.
"""
@abc.abstractmethod
def shutdown(self):
"""
This function is called when the app is being closed or the server is changing.
@@ -320,7 +321,7 @@ class Adapter(abc.ABC):
Examples of values that could be provided include ``http``, ``https``, ``file``,
or ``ftp``.
"""
# TODO actually use this
# TODO (#189) actually use this
return ()
@property

View File

@@ -23,13 +23,6 @@ from typing import (
from fuzzywuzzy import fuzz
class MediaType(Enum):
MUSIC = "music"
PODCAST = "podcast"
AUDIOBOOK = "audiobook"
VIDEO = "video"
class Genre(abc.ABC):
name: str
song_count: Optional[int]
@@ -89,29 +82,15 @@ class Song(abc.ABC):
genre: Optional[Genre]
track: Optional[int]
disc_number: Optional[int]
year: Optional[int]
cover_art: Optional[str]
size: Optional[int]
content_type: Optional[str]
suffix: Optional[str]
transcoded_content_type: Optional[str]
transcoded_suffix: Optional[str]
bit_rate: Optional[int]
is_video: Optional[bool]
user_rating: Optional[int]
average_rating: Optional[float]
play_count: Optional[int]
disc_number: Optional[int]
created: Optional[datetime]
starred: Optional[datetime]
type: Optional[MediaType]
# TODO trim down
# TODO remove distinction between Playlist and PlaylistDetails
class Playlist(abc.ABC):
# TODO trim down
id: str
name: str
song_count: Optional[int]
@@ -125,7 +104,6 @@ class Playlist(abc.ABC):
class PlaylistDetails(abc.ABC):
# TODO trim down
id: str
name: str
song_count: int

View File

@@ -31,12 +31,14 @@ class FilesystemAdapter(CachingAdapter):
@staticmethod
def get_config_parameters() -> Dict[str, ConfigParamDescriptor]:
return {
# TODO: download on play?
# TODO (#188): directory path, whether or not to scan tags
}
@staticmethod
def verify_configuration(config: Dict[str, Any]) -> Dict[str, Optional[str]]:
return {}
return {
# TODO (#188): verify that the path exists
}
def __init__(
self, config: dict, data_directory: Path, is_cache: bool = False,
@@ -73,7 +75,7 @@ class FilesystemAdapter(CachingAdapter):
is_networked = False # Can't be cached (there's no need).
can_service_requests = True # Can always be used to service requests.
# TODO make these dependent on cache state.
# TODO make these dependent on cache state. Need to do this kinda efficiently
can_get_playlists = True
can_get_playlist_details = True
can_get_cover_art_uri = True
@@ -160,7 +162,7 @@ class FilesystemAdapter(CachingAdapter):
song_model = self.get_song_details(song.id)
file = song_model.file
if file.valid and self.music_dir.joinpath(file.file_hash).exists():
# TODO check if path is permanently cached
# TODO (#74): check if path is permanently cached
return SongCacheStatus.CACHED
except Exception:
pass
@@ -457,7 +459,7 @@ class FilesystemAdapter(CachingAdapter):
def ingest_artist_data(api_artist: API.Artist) -> models.Artist:
# Ingest similar artists.
# TODO figure out which order to do this in to be msot efficient.
# TODO figure out which order to do this in to be most efficient.
if api_artist.similar_artists:
models.SimilarArtist.delete().where(
models.SimilarArtist.similar_artist.not_in(

View File

@@ -153,36 +153,11 @@ class Song(BaseModel):
return None
track = IntegerField(null=True)
disc_number = IntegerField(null=True)
year = IntegerField(null=True)
play_count = TextField(null=True)
created = TzDateTimeField(null=True)
user_rating = IntegerField(null=True)
starred = TzDateTimeField(null=True)
# TODO do I need any of these?
# size: Optional[int] = None
# content_type: Optional[str] = None
# suffix: Optional[str] = None
# transcoded_content_type: Optional[str] = None
# transcoded_suffix: Optional[str] = None
# bit_rate: Optional[int] = None
# is_video: Optional[bool] = None
# user_rating: Optional[int] = None
# average_rating: Optional[float] = None
# disc_number: Optional[int] = None
# - type_: Optional[SublimeAPI.MediaType] = None
# bookmark_position: Optional[int] = None
# original_width: Optional[int] = None
# original_height: Optional[int] = None
# class DirectoryXChildren(BaseModel):
# directory_id = ForeignKeyField(Entity)
# order = IntegerField()
# child_id = ForeignKeyField(Entity, null=True)
# class Meta:
# indexes = ((("directory_id", "order", "child_id"), True),)
class Playlist(BaseModel):
id = TextField(unique=True, primary_key=True)

View File

@@ -53,14 +53,6 @@ class SortedManyToManyQuery(ManyToManyQuery):
accessor = self._accessor
src_id = getattr(self._instance, self._src_attr)
assert not isinstance(value, SelectQuery)
# TODO DEAD CODE
# if isinstance(value, SelectQuery):
# raise NotImplementedError("Can't use a select query here")
# # query = value.columns(Value(src_id), accessor.dest_fk.rel_field)
# # accessor.through_model.insert_from(
# # fields=[accessor.src_fk, accessor.dest_fk],
# # query=query).execute()
# else:
value = ensure_tuple(value)
if not value:
return
@@ -75,31 +67,6 @@ class SortedManyToManyQuery(ManyToManyQuery):
]
accessor.through_model.insert_many(inserts).execute()
# TODO probably don't need
# def remove(self, value: Any) -> Any:
# # src_id = getattr(self._instance, self._src_attr)
# # if isinstance(value, SelectQuery):
# # column = getattr(value.model, self._dest_attr)
# # subquery = value.columns(column)
# # return (
# # self._accessor.through_model.delete().where(
# # (self._accessor.dest_fk << subquery)
# # & (self._accessor.src_fk == src_id)).execute())
# # else:
# # value = ensure_tuple(value)
# # if not value:
# # return
# # return (
# # self._accessor.through_model.delete().where(
# # (self._accessor.dest_fk << self._id_list(value))
# # & (self._accessor.src_fk == src_id)).execute())
# def clear(self) -> Any:
# src_id = getattr(self._instance, self._src_attr)
# return (
# self._accessor.through_model.delete().where(
# self._accessor.src_fk == src_id).execute())
class SortedManyToManyFieldAccessor(ManyToManyFieldAccessor):
def __get__(

View File

@@ -164,7 +164,8 @@ class AdapterManager:
def __post_init__(self):
self._download_dir = tempfile.TemporaryDirectory()
self.download_path = Path(self._download_dir.name)
# TODO can we use the threadpool executor max workersfor this
# TODO can we use the threadpool executor max workers for limiting
# downloads?
self.download_limiter_semaphore = threading.Semaphore(
self.concurrent_download_limit
)
@@ -321,7 +322,7 @@ class AdapterManager:
resource_downloading = True
AdapterManager.current_download_hashes.add(params_hash)
# TODO figure out how to retry if the other request failed.
# TODO (#122): figure out how to retry if the other request failed.
if resource_downloading:
logging.info(f"{uri} already being downloaded.")
@@ -332,7 +333,7 @@ class AdapterManager:
while params_hash in AdapterManager.current_download_hashes and t < 20:
sleep(0.2)
t += 0.2
# TODO handle the timeout
# TODO (#122): handle the timeout
else:
logging.info(f"{uri} not found. Downloading...")
try:
@@ -378,7 +379,7 @@ class AdapterManager:
@staticmethod
def _get_scheme() -> str:
# TODO eventually this will come from the players
# TODO (#189): eventually this will come from the players
assert AdapterManager._instance
scheme_priority = ("https", "http")
schemes = sorted(
@@ -452,7 +453,7 @@ class AdapterManager:
):
AdapterManager._instance.caching_adapter.invalidate_data(cache_key, args)
# TODO don't short circuit if not allow_download because it could be the
# TODO (#188): don't short circuit if not allow_download because it could be the
# filesystem adapter.
if not allow_download or not AdapterManager._ground_truth_can_do(function_name):
logging.info(f"END: NO DOWNLOAD: {function_name}")
@@ -478,8 +479,6 @@ class AdapterManager:
logging.debug(result)
return result
# TODO abstract more stuff
# Usage and Availability Properties
# ==================================================================================
@staticmethod
@@ -626,7 +625,7 @@ class AdapterManager:
@staticmethod
def delete_playlist(playlist_id: str):
# TODO: make non-blocking?
# TODO (#190): make non-blocking?
assert AdapterManager._instance
AdapterManager._instance.ground_truth_adapter.delete_playlist(playlist_id)
@@ -635,7 +634,8 @@ class AdapterManager:
CachingAdapter.CachedDataKey.PLAYLIST_DETAILS, (playlist_id,)
)
# TODO allow this to take a set of schemes and unify with get_cover_art_filename
# TODO (#189): allow this to take a set of schemes and unify with
# get_cover_art_filename
@staticmethod
def get_cover_art_uri(cover_art_id: str = None) -> str:
assert AdapterManager._instance
@@ -655,7 +655,7 @@ class AdapterManager:
before_download: Callable[[], None] = lambda: None,
force: bool = False, # TODO: rename to use_ground_truth_adapter?
allow_download: bool = True,
) -> Result[str]: # TODO: convert to return bytes?
) -> Result[str]:
existing_cover_art_filename = str(
Path(__file__).parent.joinpath("images/default-album-art.png")
)
@@ -718,21 +718,18 @@ class AdapterManager:
return future
# TODO allow this to take a set of schemes
# TODO (#189): allow this to take a set of schemes
@staticmethod
def get_song_filename_or_stream(
song: Song, format: str = None, force_stream: bool = False,
) -> Tuple[str, bool]: # TODO probably don't need to return a tuple anymore
) -> str:
assert AdapterManager._instance
cached_song_filename = None
if AdapterManager._can_use_cache(force_stream, "get_song_uri"):
assert AdapterManager._instance.caching_adapter
try:
return (
AdapterManager._instance.caching_adapter.get_song_uri(
song.id, "file"
),
False,
return AdapterManager._instance.caching_adapter.get_song_uri(
song.id, "file"
)
except CacheMissError as e:
if e.partial_data is not None:
@@ -746,7 +743,7 @@ class AdapterManager:
if not AdapterManager._ground_truth_can_do("get_song_uri"):
if force_stream or cached_song_filename is None:
raise Exception("Can't stream the song.")
return (cached_song_filename, False)
return cached_song_filename
# TODO implement subsonic extension to get the hash of the song and compare
# here. That way of the cache gets blown away, but not the song files, it will
@@ -755,11 +752,8 @@ class AdapterManager:
if force_stream and not AdapterManager._ground_truth_can_do("stream"):
raise Exception("Can't stream the song.")
return (
AdapterManager._instance.ground_truth_adapter.get_song_uri(
song.id, AdapterManager._get_scheme(), stream=True,
),
True,
return AdapterManager._instance.ground_truth_adapter.get_song_uri(
song.id, AdapterManager._get_scheme(), stream=True,
)
@staticmethod

View File

@@ -441,6 +441,7 @@ class SubsonicAdapter(Adapter):
if directory_id == "root":
return self._get_indexes()
# TODO (#187) make sure to filter out all non-song files
directory = self._get_json(
self._make_url("getMusicDirectory"), id=directory_id
).directory

View File

@@ -160,6 +160,7 @@ class Song(SublimeAPI.Song, DataClassJsonMixin):
title: str = field(metadata=config(field_name="name"))
path: Optional[str] = None
parent_id: Optional[str] = field(default=None, metadata=config(field_name="parent"))
duration: Optional[timedelta] = None
# Artist
artist: Optional[ArtistAndArtistInfo] = field(init=False)
@@ -175,25 +176,12 @@ class Song(SublimeAPI.Song, DataClassJsonMixin):
genre: Optional[Genre] = field(init=False)
_genre: Optional[str] = field(default=None, metadata=config(field_name="genre"))
# TODO deal with these
track: Optional[int] = None
disc_number: Optional[int] = None
year: Optional[int] = None
cover_art: Optional[str] = None
size: Optional[int] = None
content_type: Optional[str] = None
suffix: Optional[str] = None
transcoded_content_type: Optional[str] = None
transcoded_suffix: Optional[str] = None
duration: Optional[timedelta] = None
bit_rate: Optional[int] = None
is_video: Optional[bool] = None
user_rating: Optional[int] = None
average_rating: Optional[float] = None
play_count: Optional[int] = None
disc_number: Optional[int] = None
created: Optional[datetime] = None
starred: Optional[datetime] = None
type: Optional[SublimeAPI.MediaType] = None
def __post_init__(self):
self.parent_id = (self.parent_id or "root") if self.id != "root" else None

View File

@@ -15,13 +15,6 @@ try:
except Exception:
tap_imported = False
try:
# import keyring
has_keyring = True
except ImportError:
has_keyring = False
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk
try:
@@ -225,7 +218,7 @@ class SublimeMusicApp(Gtk.Application):
self.player = self.mpv_player
if self.app_config.state.current_device != "this device":
# TODO (#120)
# TODO (#120) attempt to connect to the previously connected device
pass
self.app_config.state.current_device = "this device"
@@ -670,21 +663,6 @@ class SublimeMusicApp(Gtk.Application):
self.update_window()
def on_server_list_changed(self, action: Any, servers: GLib.Variant):
# TODO do the save to the keyring here
# keyring.set_password(
# 'com.sumnerevans.SublimeMusic',
# f'{self.username}@{self.server_address}',
# password,
# )
# def get_password(self) -> str:
# try:
# return keyring.get_password(
# 'com.sumnerevans.SublimeMusic',
# f'{self.username}@{self.server_address}',
# )
# except Exception:
# return self.password
self.app_config.servers = servers
self.app_config.save()
@@ -989,7 +967,7 @@ class SublimeMusicApp(Gtk.Application):
if order_token != self.song_playing_order_token:
return
uri, _ = AdapterManager.get_song_filename_or_stream(
uri = AdapterManager.get_song_filename_or_stream(
song, force_stream=self.app_config.always_stream,
)
@@ -1081,8 +1059,8 @@ class SublimeMusicApp(Gtk.Application):
# Hotswap to the downloaded song.
if (
# TODO allow hotswap if not playing. This requires being able to
# replace the currently playing URI with something different.
# TODO (#182) allow hotswap if not playing. This requires being able
# to replace the currently playing URI with something different.
self.app_config.state.playing
and self.app_config.state.current_song
and self.app_config.state.current_song.id == song_id
@@ -1093,7 +1071,7 @@ class SublimeMusicApp(Gtk.Application):
assert self.player
if self.player.can_hotswap_source:
self.player.play_media(
AdapterManager.get_song_filename_or_stream(song)[0],
AdapterManager.get_song_filename_or_stream(song),
self.app_config.state.song_progress,
song,
)

View File

@@ -298,8 +298,8 @@ class DBusManager:
"mpris:trackid": trackid,
"mpris:length": duration,
"mpris:artUrl": cover_art,
# TODO use walrus once MYPY isn't retarded
"xesam:album": song.album.name if song.album else "",
# TODO (#71) use walrus once MYPY isn't retarded
"xesam:album": (song.album.name if song.album else ""),
"xesam:albumArtist": [artist_name],
"xesam:artist": artist_name,
"xesam:title": song.title,

View File

@@ -1,88 +0,0 @@
import typing
from datetime import datetime
from enum import EnumMeta
from typing import Any, Dict, Type
from dateutil import parser
def from_json(template_type: Any, data: Any) -> Any:
"""
Converts data from a JSON parse into an instantiation of the Python object specified
by template_type.
:param template_type: the template type to deserialize into
:param data: the data to deserialize to the class
"""
# Approach for deserialization here:
# https://stackoverflow.com/a/40639688/2319844
# If it's a forward reference, evaluate it to figure out the actual
# type. This allows for types that have to be put into a string.
if isinstance(template_type, typing.ForwardRef): # type: ignore
template_type = template_type._evaluate(globals(), locals())
annotations: Dict[str, Type] = getattr(template_type, "__annotations__", {})
# Handle primitive of objects
instance: Any = None
if data is None:
instance = None
# Handle generics. List[*], Dict[*, *] in particular.
elif type(template_type) == typing._GenericAlias: # type: ignore
if template_type.__origin__ == typing.Union:
template_type = template_type.__args__[0]
instance = from_json(template_type, data)
else:
# Having to use this because things changed in Python 3.7.
class_name = template_type._name
# This is not very elegant since it doesn't allow things which
# sublass from List or Dict. For my purposes, this doesn't matter.
if class_name == "List":
inner_type = template_type.__args__[0]
instance = [from_json(inner_type, value) for value in data]
elif class_name == "Dict":
key_type, val_type = template_type.__args__
instance = {
from_json(key_type, key): from_json(val_type, value)
for key, value in data.items()
}
else:
raise Exception(
"Trying to deserialize an unsupported type: {}".format(
template_type._name
)
)
elif template_type == str or issubclass(template_type, str):
instance = data
elif template_type == int or issubclass(template_type, int):
instance = int(data)
elif template_type == bool or issubclass(template_type, bool):
instance = bool(data)
elif type(template_type) == EnumMeta:
if type(data) == dict:
instance = template_type(data.get("_value_"))
else:
instance = template_type(data)
elif template_type == datetime:
if type(data) == int:
instance = datetime.fromtimestamp(data / 1000)
else:
instance = parser.parse(data)
# Handle everything else by first instantiating the class, then adding
# all of the sub-elements, recursively calling from_json on them.
else:
# for field, field_type in annotations.items():
# value = data.get(field)
# setattr(instance, field, from_json(field_type, value))
instance = template_type(
**{
field: from_json(field_type, data.get(field))
for field, field_type in annotations.items()
}
)
return instance

View File

@@ -268,7 +268,7 @@ class ChromecastPlayer(Player):
# the local filesystem is disabled and set it to ("file", "http",
# "https") in the other case.
song = AdapterManager.get_song_details(self.song_id).result()
filename, _ = AdapterManager.get_song_filename_or_stream(song)
filename = AdapterManager.get_song_filename_or_stream(song)
with open(filename, "rb") as fin:
song_buffer = io.BytesIO(fin.read())
@@ -443,7 +443,7 @@ class ChromecastPlayer(Player):
self.server_thread.set_song_and_token(song.id, token)
file_or_url = f"http://{self.host_ip}:{self.port}/s/{token}"
else:
file_or_url, _ = AdapterManager.get_song_filename_or_stream(
file_or_url = AdapterManager.get_song_filename_or_stream(
song, force_stream=True,
)

View File

@@ -89,7 +89,7 @@ class AlbumsPanel(Gtk.Box):
)
actionbar.pack_start(self.alphabetical_type_combo)
# TODO: Alphabetically?
# TODO: Sort genre combo box alphabetically?
self.genre_combo, self.genre_combo_store = self.make_combobox(
(), self.on_genre_change
)
@@ -167,7 +167,8 @@ class AlbumsPanel(Gtk.Box):
self.updating_query = False
# Never force. We invalidate the cache ourselves (force is used when
# sort params change). TODO
# sort params change). TODO I don't think taat is the case now probaly can just
# force=force here
genres_future = AdapterManager.get_genres(force=False)
genres_future.add_done_callback(lambda f: GLib.idle_add(get_genres_done, f))

View File

@@ -136,7 +136,7 @@ class PlayerControls(Gtk.ActionBar):
)
self.song_title.set_markup(util.esc(app_config.state.current_song.title))
# TODO use walrus once MYPY gets its act together
# TODO (#71): use walrus once MYPY gets its act together
album = app_config.state.current_song.album
artist = app_config.state.current_song.artist
if album:
@@ -187,7 +187,7 @@ class PlayerControls(Gtk.ActionBar):
def calculate_label(song_details: Song) -> str:
title = util.esc(song_details.title)
# TODO: use walrus once MYPY works with this
# TODO (#71): use walrus once MYPY works with this
# album = util.esc(album.name if (album := song_details.album) else None)
# artist = util.esc(artist.name if (artist := song_details.artist) else None) # noqa
album = util.esc(song_details.album.name if song_details.album else None)

View File

@@ -169,13 +169,7 @@ def test_get_playlist_details(adapter: SubsonicAdapter):
year=2016,
_genre="Christian & Gospel",
cover_art="318",
size=8381640,
content_type="audio/mp4",
suffix="m4a",
transcoded_content_type="audio/mpeg",
transcoded_suffix="mp3",
duration=timedelta(seconds=238),
bit_rate=256,
path="/".join(
(
"Hillsong Worship",
@@ -183,11 +177,7 @@ def test_get_playlist_details(adapter: SubsonicAdapter):
"01 What a Beautiful Name.m4a",
)
),
is_video=False,
play_count=20,
disc_number=1,
created=datetime(2020, 3, 27, 5, 17, 7, tzinfo=timezone.utc),
type=SubsonicAPI.SublimeAPI.MediaType.MUSIC,
)
@@ -212,13 +202,7 @@ def test_create_playlist(adapter: SubsonicAdapter):
year=2016,
_genre="Christian & Gospel",
cover_art="318",
size=8381640,
content_type="audio/mp4",
suffix="m4a",
transcoded_content_type="audio/mpeg",
transcoded_suffix="mp3",
duration=timedelta(seconds=238),
bit_rate=256,
path="/".join(
(
"Hillsong Worship",
@@ -226,11 +210,7 @@ def test_create_playlist(adapter: SubsonicAdapter):
"01 What a Beautiful Name.m4a",
)
),
is_video=False,
play_count=20,
disc_number=1,
created=datetime(2020, 3, 27, 5, 17, 7, tzinfo=timezone.utc),
type=SubsonicAPI.SublimeAPI.MediaType.MUSIC,
)
],
)