A bunch of cleanup
This commit is contained in:
116
Pipfile.lock
generated
116
Pipfile.lock
generated
@@ -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": [
|
||||
|
@@ -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
|
||||
|
2
setup.py
2
setup.py
@@ -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",
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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(
|
||||
|
@@ -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)
|
||||
|
@@ -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__(
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
@@ -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,
|
||||
)
|
||||
|
||||
|
@@ -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))
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
Reference in New Issue
Block a user