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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,12 +31,14 @@ class FilesystemAdapter(CachingAdapter):
@staticmethod @staticmethod
def get_config_parameters() -> Dict[str, ConfigParamDescriptor]: def get_config_parameters() -> Dict[str, ConfigParamDescriptor]:
return { return {
# TODO: download on play? # TODO (#188): directory path, whether or not to scan tags
} }
@staticmethod @staticmethod
def verify_configuration(config: Dict[str, Any]) -> Dict[str, Optional[str]]: def verify_configuration(config: Dict[str, Any]) -> Dict[str, Optional[str]]:
return {} return {
# TODO (#188): verify that the path exists
}
def __init__( def __init__(
self, config: dict, data_directory: Path, is_cache: bool = False, 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). is_networked = False # Can't be cached (there's no need).
can_service_requests = True # Can always be used to service requests. 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_playlists = True
can_get_playlist_details = True can_get_playlist_details = True
can_get_cover_art_uri = True can_get_cover_art_uri = True
@@ -160,7 +162,7 @@ class FilesystemAdapter(CachingAdapter):
song_model = self.get_song_details(song.id) song_model = self.get_song_details(song.id)
file = song_model.file file = song_model.file
if file.valid and self.music_dir.joinpath(file.file_hash).exists(): 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 return SongCacheStatus.CACHED
except Exception: except Exception:
pass pass
@@ -457,7 +459,7 @@ class FilesystemAdapter(CachingAdapter):
def ingest_artist_data(api_artist: API.Artist) -> models.Artist: def ingest_artist_data(api_artist: API.Artist) -> models.Artist:
# Ingest similar artists. # 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: if api_artist.similar_artists:
models.SimilarArtist.delete().where( models.SimilarArtist.delete().where(
models.SimilarArtist.similar_artist.not_in( models.SimilarArtist.similar_artist.not_in(

View File

@@ -153,36 +153,11 @@ class Song(BaseModel):
return None return None
track = IntegerField(null=True) track = IntegerField(null=True)
disc_number = IntegerField(null=True)
year = IntegerField(null=True) year = IntegerField(null=True)
play_count = TextField(null=True) user_rating = IntegerField(null=True)
created = TzDateTimeField(null=True)
starred = TzDateTimeField(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): class Playlist(BaseModel):
id = TextField(unique=True, primary_key=True) id = TextField(unique=True, primary_key=True)

View File

@@ -53,14 +53,6 @@ class SortedManyToManyQuery(ManyToManyQuery):
accessor = self._accessor accessor = self._accessor
src_id = getattr(self._instance, self._src_attr) src_id = getattr(self._instance, self._src_attr)
assert not isinstance(value, SelectQuery) 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) value = ensure_tuple(value)
if not value: if not value:
return return
@@ -75,31 +67,6 @@ class SortedManyToManyQuery(ManyToManyQuery):
] ]
accessor.through_model.insert_many(inserts).execute() 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): class SortedManyToManyFieldAccessor(ManyToManyFieldAccessor):
def __get__( def __get__(

View File

@@ -164,7 +164,8 @@ class AdapterManager:
def __post_init__(self): def __post_init__(self):
self._download_dir = tempfile.TemporaryDirectory() self._download_dir = tempfile.TemporaryDirectory()
self.download_path = Path(self._download_dir.name) 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.download_limiter_semaphore = threading.Semaphore(
self.concurrent_download_limit self.concurrent_download_limit
) )
@@ -321,7 +322,7 @@ class AdapterManager:
resource_downloading = True resource_downloading = True
AdapterManager.current_download_hashes.add(params_hash) 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: if resource_downloading:
logging.info(f"{uri} already being downloaded.") logging.info(f"{uri} already being downloaded.")
@@ -332,7 +333,7 @@ class AdapterManager:
while params_hash in AdapterManager.current_download_hashes and t < 20: while params_hash in AdapterManager.current_download_hashes and t < 20:
sleep(0.2) sleep(0.2)
t += 0.2 t += 0.2
# TODO handle the timeout # TODO (#122): handle the timeout
else: else:
logging.info(f"{uri} not found. Downloading...") logging.info(f"{uri} not found. Downloading...")
try: try:
@@ -378,7 +379,7 @@ class AdapterManager:
@staticmethod @staticmethod
def _get_scheme() -> str: def _get_scheme() -> str:
# TODO eventually this will come from the players # TODO (#189): eventually this will come from the players
assert AdapterManager._instance assert AdapterManager._instance
scheme_priority = ("https", "http") scheme_priority = ("https", "http")
schemes = sorted( schemes = sorted(
@@ -452,7 +453,7 @@ class AdapterManager:
): ):
AdapterManager._instance.caching_adapter.invalidate_data(cache_key, args) 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. # filesystem adapter.
if not allow_download or not AdapterManager._ground_truth_can_do(function_name): if not allow_download or not AdapterManager._ground_truth_can_do(function_name):
logging.info(f"END: NO DOWNLOAD: {function_name}") logging.info(f"END: NO DOWNLOAD: {function_name}")
@@ -478,8 +479,6 @@ class AdapterManager:
logging.debug(result) logging.debug(result)
return result return result
# TODO abstract more stuff
# Usage and Availability Properties # Usage and Availability Properties
# ================================================================================== # ==================================================================================
@staticmethod @staticmethod
@@ -626,7 +625,7 @@ class AdapterManager:
@staticmethod @staticmethod
def delete_playlist(playlist_id: str): def delete_playlist(playlist_id: str):
# TODO: make non-blocking? # TODO (#190): make non-blocking?
assert AdapterManager._instance assert AdapterManager._instance
AdapterManager._instance.ground_truth_adapter.delete_playlist(playlist_id) AdapterManager._instance.ground_truth_adapter.delete_playlist(playlist_id)
@@ -635,7 +634,8 @@ class AdapterManager:
CachingAdapter.CachedDataKey.PLAYLIST_DETAILS, (playlist_id,) 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 @staticmethod
def get_cover_art_uri(cover_art_id: str = None) -> str: def get_cover_art_uri(cover_art_id: str = None) -> str:
assert AdapterManager._instance assert AdapterManager._instance
@@ -655,7 +655,7 @@ class AdapterManager:
before_download: Callable[[], None] = lambda: None, before_download: Callable[[], None] = lambda: None,
force: bool = False, # TODO: rename to use_ground_truth_adapter? force: bool = False, # TODO: rename to use_ground_truth_adapter?
allow_download: bool = True, allow_download: bool = True,
) -> Result[str]: # TODO: convert to return bytes? ) -> Result[str]:
existing_cover_art_filename = str( existing_cover_art_filename = str(
Path(__file__).parent.joinpath("images/default-album-art.png") Path(__file__).parent.joinpath("images/default-album-art.png")
) )
@@ -718,21 +718,18 @@ class AdapterManager:
return future return future
# TODO allow this to take a set of schemes # TODO (#189): allow this to take a set of schemes
@staticmethod @staticmethod
def get_song_filename_or_stream( def get_song_filename_or_stream(
song: Song, format: str = None, force_stream: bool = False, 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 assert AdapterManager._instance
cached_song_filename = None cached_song_filename = None
if AdapterManager._can_use_cache(force_stream, "get_song_uri"): if AdapterManager._can_use_cache(force_stream, "get_song_uri"):
assert AdapterManager._instance.caching_adapter assert AdapterManager._instance.caching_adapter
try: try:
return ( return AdapterManager._instance.caching_adapter.get_song_uri(
AdapterManager._instance.caching_adapter.get_song_uri( song.id, "file"
song.id, "file"
),
False,
) )
except CacheMissError as e: except CacheMissError as e:
if e.partial_data is not None: if e.partial_data is not None:
@@ -746,7 +743,7 @@ class AdapterManager:
if not AdapterManager._ground_truth_can_do("get_song_uri"): if not AdapterManager._ground_truth_can_do("get_song_uri"):
if force_stream or cached_song_filename is None: if force_stream or cached_song_filename is None:
raise Exception("Can't stream the song.") 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 # 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 # 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"): if force_stream and not AdapterManager._ground_truth_can_do("stream"):
raise Exception("Can't stream the song.") raise Exception("Can't stream the song.")
return ( return AdapterManager._instance.ground_truth_adapter.get_song_uri(
AdapterManager._instance.ground_truth_adapter.get_song_uri( song.id, AdapterManager._get_scheme(), stream=True,
song.id, AdapterManager._get_scheme(), stream=True,
),
True,
) )
@staticmethod @staticmethod

View File

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

View File

@@ -160,6 +160,7 @@ class Song(SublimeAPI.Song, DataClassJsonMixin):
title: str = field(metadata=config(field_name="name")) title: str = field(metadata=config(field_name="name"))
path: Optional[str] = None path: Optional[str] = None
parent_id: Optional[str] = field(default=None, metadata=config(field_name="parent")) parent_id: Optional[str] = field(default=None, metadata=config(field_name="parent"))
duration: Optional[timedelta] = None
# Artist # Artist
artist: Optional[ArtistAndArtistInfo] = field(init=False) artist: Optional[ArtistAndArtistInfo] = field(init=False)
@@ -175,25 +176,12 @@ class Song(SublimeAPI.Song, DataClassJsonMixin):
genre: Optional[Genre] = field(init=False) genre: Optional[Genre] = field(init=False)
_genre: Optional[str] = field(default=None, metadata=config(field_name="genre")) _genre: Optional[str] = field(default=None, metadata=config(field_name="genre"))
# TODO deal with these
track: Optional[int] = None track: Optional[int] = None
disc_number: Optional[int] = None
year: Optional[int] = None year: Optional[int] = None
cover_art: Optional[str] = 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 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 starred: Optional[datetime] = None
type: Optional[SublimeAPI.MediaType] = None
def __post_init__(self): def __post_init__(self):
self.parent_id = (self.parent_id or "root") if self.id != "root" else None self.parent_id = (self.parent_id or "root") if self.id != "root" else None

View File

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

View File

@@ -298,8 +298,8 @@ class DBusManager:
"mpris:trackid": trackid, "mpris:trackid": trackid,
"mpris:length": duration, "mpris:length": duration,
"mpris:artUrl": cover_art, "mpris:artUrl": cover_art,
# TODO use walrus once MYPY isn't retarded # TODO (#71) use walrus once MYPY isn't retarded
"xesam:album": song.album.name if song.album else "", "xesam:album": (song.album.name if song.album else ""),
"xesam:albumArtist": [artist_name], "xesam:albumArtist": [artist_name],
"xesam:artist": artist_name, "xesam:artist": artist_name,
"xesam:title": song.title, "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", # the local filesystem is disabled and set it to ("file", "http",
# "https") in the other case. # "https") in the other case.
song = AdapterManager.get_song_details(self.song_id).result() 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: with open(filename, "rb") as fin:
song_buffer = io.BytesIO(fin.read()) song_buffer = io.BytesIO(fin.read())
@@ -443,7 +443,7 @@ class ChromecastPlayer(Player):
self.server_thread.set_song_and_token(song.id, token) self.server_thread.set_song_and_token(song.id, token)
file_or_url = f"http://{self.host_ip}:{self.port}/s/{token}" file_or_url = f"http://{self.host_ip}:{self.port}/s/{token}"
else: else:
file_or_url, _ = AdapterManager.get_song_filename_or_stream( file_or_url = AdapterManager.get_song_filename_or_stream(
song, force_stream=True, song, force_stream=True,
) )

View File

@@ -89,7 +89,7 @@ class AlbumsPanel(Gtk.Box):
) )
actionbar.pack_start(self.alphabetical_type_combo) 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.genre_combo, self.genre_combo_store = self.make_combobox(
(), self.on_genre_change (), self.on_genre_change
) )
@@ -167,7 +167,8 @@ class AlbumsPanel(Gtk.Box):
self.updating_query = False self.updating_query = False
# Never force. We invalidate the cache ourselves (force is used when # 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 = AdapterManager.get_genres(force=False)
genres_future.add_done_callback(lambda f: GLib.idle_add(get_genres_done, f)) 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)) 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 album = app_config.state.current_song.album
artist = app_config.state.current_song.artist artist = app_config.state.current_song.artist
if album: if album:
@@ -187,7 +187,7 @@ class PlayerControls(Gtk.ActionBar):
def calculate_label(song_details: Song) -> str: def calculate_label(song_details: Song) -> str:
title = util.esc(song_details.title) 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) # 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 # 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) 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, year=2016,
_genre="Christian & Gospel", _genre="Christian & Gospel",
cover_art="318", cover_art="318",
size=8381640,
content_type="audio/mp4",
suffix="m4a",
transcoded_content_type="audio/mpeg",
transcoded_suffix="mp3",
duration=timedelta(seconds=238), duration=timedelta(seconds=238),
bit_rate=256,
path="/".join( path="/".join(
( (
"Hillsong Worship", "Hillsong Worship",
@@ -183,11 +177,7 @@ def test_get_playlist_details(adapter: SubsonicAdapter):
"01 What a Beautiful Name.m4a", "01 What a Beautiful Name.m4a",
) )
), ),
is_video=False,
play_count=20,
disc_number=1, 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, year=2016,
_genre="Christian & Gospel", _genre="Christian & Gospel",
cover_art="318", cover_art="318",
size=8381640,
content_type="audio/mp4",
suffix="m4a",
transcoded_content_type="audio/mpeg",
transcoded_suffix="mp3",
duration=timedelta(seconds=238), duration=timedelta(seconds=238),
bit_rate=256,
path="/".join( path="/".join(
( (
"Hillsong Worship", "Hillsong Worship",
@@ -226,11 +210,7 @@ def test_create_playlist(adapter: SubsonicAdapter):
"01 What a Beautiful Name.m4a", "01 What a Beautiful Name.m4a",
) )
), ),
is_video=False,
play_count=20,
disc_number=1, disc_number=1,
created=datetime(2020, 3, 27, 5, 17, 7, tzinfo=timezone.utc),
type=SubsonicAPI.SublimeAPI.MediaType.MUSIC,
) )
], ],
) )