Merge branch 'srht-builds' into 218-python3.8-flatpak
52
.builds/build.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
image: archlinux
|
||||
packages:
|
||||
- dbus
|
||||
- gobject-introspection
|
||||
- gtk3
|
||||
- mpv
|
||||
- python-cairo
|
||||
- python-gobject
|
||||
- python-poetry
|
||||
- xorg-server-xvfb
|
||||
sources:
|
||||
- https://git.sr.ht/~sumner/sublime-music
|
||||
environment:
|
||||
REPO_NAME: sublime-music
|
||||
triggers:
|
||||
- action: email
|
||||
condition: failure
|
||||
to: ~sumner/sublime-music-devel@lists.sr.ht
|
||||
tasks:
|
||||
- setup: |
|
||||
cd ${REPO_NAME}
|
||||
poetry install
|
||||
echo "cd ${REPO_NAME}" >> ~/.buildenv
|
||||
echo "source $(poetry env info -p)/bin/activate" >> ~/.buildenv
|
||||
|
||||
- lint: |
|
||||
python setup.py check -mrs
|
||||
black --check .
|
||||
flake8
|
||||
mypy sublime_music tests/**/*.py
|
||||
cicd/custom_style_check.py
|
||||
|
||||
- test: |
|
||||
Xvfb :119 -screen 0 1024x768x16 &
|
||||
export DISPLAY=:119
|
||||
pytest
|
||||
|
||||
- build: |
|
||||
python setup.py sdist
|
||||
|
||||
- build-flatpak: |
|
||||
cd ${REPO_NAME}/flatpak
|
||||
# REPO=repo ./flatpak_build.sh
|
||||
|
||||
- deploy-pypi: |
|
||||
./cicd/run_if_tagged_with_version \
|
||||
"twine upload -r testpypi dist/*" \
|
||||
"twine upload dist/*"
|
||||
|
||||
- verify-pypi: |
|
||||
./cicd/run_if_tagged_with_version \
|
||||
"pip install ${REPO_NAME}"
|
40
.builds/gitlab-mirror-and-readme.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
image: alpine/edge
|
||||
packages:
|
||||
- curl
|
||||
- git
|
||||
- openssh
|
||||
- py3-docutils
|
||||
sources:
|
||||
- https://git.sr.ht/~sumner/sublime-music
|
||||
secrets:
|
||||
# README Personal Access Token
|
||||
- 2fb5fd72-fa96-46c6-ab90-6b7cabebba16
|
||||
# GitLab Mirror SSH Key
|
||||
- 910786ff-96e4-4951-be00-ad99cc02f357
|
||||
environment:
|
||||
REPO_NAME: sublime-music
|
||||
# triggers:
|
||||
# - action: email
|
||||
# condition: failure
|
||||
# to: ~sumner/sublime-music-devel@lists.sr.ht
|
||||
tasks:
|
||||
- setup: |
|
||||
cd ${REPO_NAME}
|
||||
echo "cd ${REPO_NAME}" >> ~/.buildenv
|
||||
|
||||
# - gitlab-mirror: |
|
||||
# ssh-keyscan gitlab.com >> ~/.ssh/known_hosts
|
||||
# git push --quiet --mirror git@gitlab.com:sublime-music/sublime-music.git
|
||||
|
||||
# If we are on the master branch, compile the README.rst to HTML and set it
|
||||
# as the README for the repo.
|
||||
- readme: |
|
||||
set +x
|
||||
git branch --contains | grep master &&
|
||||
rst2html5 --no-doc-title README.rst | \
|
||||
curl -H "Content-Type: text/html" \
|
||||
-H "Authorization: Bearer $(cat ~/.readme-token)" \
|
||||
-XPUT \
|
||||
--data-binary @- \
|
||||
"https://git.sr.ht/api/repos/${PROJECT_NAME}/readme" &&
|
||||
echo "README set" || echo "Skipping README set because not on master"
|
7
.envrc
Normal file
@@ -0,0 +1,7 @@
|
||||
# Run poetry install and activate the virtualenv
|
||||
poetry install
|
||||
source .venv/bin/activate
|
||||
|
||||
watch_file pyproject.toml
|
||||
watch_file poetry.lock
|
||||
watch_file setup.py
|
27
cicd/run_if_tagged_with_version
Executable file
@@ -0,0 +1,27 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
version_tag_re = re.compile(r"v\d+\.\d+\.\d+")
|
||||
|
||||
|
||||
tags = (
|
||||
subprocess.run(["git", "tag", "--contains", "HEAD"], capture_output=True)
|
||||
.stdout.decode()
|
||||
.strip()
|
||||
.split()
|
||||
)
|
||||
|
||||
# If one of the tags is a version tag, then run the commands specified in the
|
||||
# parameters.
|
||||
for tag in tags:
|
||||
if match := version_tag_re.match(tag):
|
||||
print(f"VERSION TAG {tag} FOUND")
|
||||
|
||||
# Execute the associated commands, raising an exception if the command
|
||||
# returns a non-zero value.
|
||||
for arg in sys.argv[1:]:
|
||||
print(f"+ {' '.join(arg.split())}")
|
||||
subprocess.run(arg.split()).check_returncode()
|
1118
poetry.lock
generated
Normal file
@@ -1,3 +1,67 @@
|
||||
[tool.poetry]
|
||||
name = "sublime_music"
|
||||
version = "0.11.9"
|
||||
description = "A native GTK *sonic client."
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Sumner Evans <inquiries@sumnerevans.com>"]
|
||||
readme = "README.rst"
|
||||
homepage = "https://sublimemusic.app"
|
||||
repository = "https://sr.ht/~sumner/sublime-music"
|
||||
documentation = "https://sublime-music.gitlab.io/sublime-music/"
|
||||
keywords = ["airsonic", "music", "chromecast", "subsonic"]
|
||||
classifiers = [
|
||||
# 3 - Alpha
|
||||
# 4 - Beta
|
||||
# 5 - Production/Stable
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Operating System :: POSIX",
|
||||
]
|
||||
|
||||
# TODO
|
||||
exclude = ["tests"]
|
||||
|
||||
[tool.poetry.urls]
|
||||
"Bug Tracker" = "https://todo.sr.ht/~sumner/sublime-music"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
sublime-music = 'sublime_music.__main__:main'
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
bleach = "^3.2.1"
|
||||
dataclasses-json = "^0.5.2"
|
||||
deepdiff = "^5.0.2"
|
||||
fuzzywuzzy = "^0.18.0"
|
||||
peewee = "^3.13.3"
|
||||
PyGObject = "^3.38.0"
|
||||
python-dateutil = "^2.8.1"
|
||||
python-Levenshtein = "^0.12.0"
|
||||
python-mpv = "^0.5.2"
|
||||
requests = "^2.24.0"
|
||||
semver = "^2.10.2"
|
||||
bottle = {version = "^0.12.18", optional = true}
|
||||
keyring = {version = "^21.4.0", optional = true}
|
||||
pychromecast = {version = "^7.3.0", optional = true}
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^20.8b1"
|
||||
docutils = "^0.16"
|
||||
flake8 = "^3.8.3"
|
||||
mypy = "^0.782"
|
||||
pytest-cov = "^2.10.1"
|
||||
termcolor = "^1.1.0"
|
||||
requirements-parser = "^0.2.0"
|
||||
|
||||
[tool.poetry.extras]
|
||||
chromecast = ["pychromecast"]
|
||||
keyring = ["keyring"]
|
||||
server = ["bottle"]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
||||
|
||||
[tool.black]
|
||||
exclude = '''
|
||||
(
|
||||
|
18
setup.py
@@ -8,19 +8,19 @@ with open(here.joinpath("README.rst"), encoding="utf-8") as f:
|
||||
long_description = f.read()
|
||||
|
||||
# Find the version
|
||||
with open(here.joinpath("sublime", "__init__.py")) as f:
|
||||
with open(here.joinpath("sublime_music", "__init__.py")) as f:
|
||||
for line in f:
|
||||
if line.startswith("__version__"):
|
||||
version = eval(line.split()[-1])
|
||||
break
|
||||
|
||||
package_data_dirs = [
|
||||
here.joinpath("sublime", "adapters", "icons"),
|
||||
here.joinpath("sublime", "adapters", "images"),
|
||||
here.joinpath("sublime", "adapters", "subsonic", "icons"),
|
||||
here.joinpath("sublime", "dbus", "mpris_specs"),
|
||||
here.joinpath("sublime", "ui", "icons"),
|
||||
here.joinpath("sublime", "ui", "images"),
|
||||
here.joinpath("sublime_music", "adapters", "icons"),
|
||||
here.joinpath("sublime_music", "adapters", "images"),
|
||||
here.joinpath("sublime_music", "adapters", "subsonic", "icons"),
|
||||
here.joinpath("sublime_music", "dbus", "mpris_specs"),
|
||||
here.joinpath("sublime_music", "ui", "icons"),
|
||||
here.joinpath("sublime_music", "ui", "images"),
|
||||
]
|
||||
package_data_files = []
|
||||
for data_dir in package_data_dirs:
|
||||
@@ -28,7 +28,7 @@ for data_dir in package_data_dirs:
|
||||
package_data_files.append(str(file))
|
||||
|
||||
setup(
|
||||
name="sublime-music",
|
||||
name="sublime_music",
|
||||
version=version,
|
||||
url="https://gitlab.com/sublime-music/sublime-music",
|
||||
description="A native GTK *sonic client.",
|
||||
@@ -51,7 +51,7 @@ setup(
|
||||
],
|
||||
keywords="airsonic subsonic libresonic gonic music",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
package_data={"sublime": ["ui/app_styles.css", *package_data_files]},
|
||||
package_data={"sublime_music": ["ui/app_styles.css", *package_data_files]},
|
||||
install_requires=[
|
||||
"bleach",
|
||||
"dataclasses-json",
|
||||
|
@@ -574,7 +574,7 @@ class Adapter(abc.ABC):
|
||||
"""
|
||||
raise self._check_can_error("get_playlists")
|
||||
|
||||
def get_playlist_details(self, playlist_id: str,) -> Playlist:
|
||||
def get_playlist_details(self, playlist_id: str) -> Playlist:
|
||||
"""
|
||||
Get the details for the given ``playlist_id``. If the playlist_id does not
|
||||
exist, then this function should throw an exception.
|
||||
@@ -586,7 +586,7 @@ class Adapter(abc.ABC):
|
||||
raise self._check_can_error("get_playlist_details")
|
||||
|
||||
def create_playlist(
|
||||
self, name: str, songs: Sequence[Song] = None,
|
||||
self, name: str, songs: Sequence[Song] = None
|
||||
) -> Optional[Playlist]:
|
||||
"""
|
||||
Creates a playlist of the given name with the given songs.
|
@@ -178,7 +178,9 @@ class SearchResult:
|
||||
_S = TypeVar("_S")
|
||||
|
||||
def _to_result(
|
||||
self, it: Dict[str, _S], transform: Callable[[_S], Tuple[Optional[str], ...]],
|
||||
self,
|
||||
it: Dict[str, _S],
|
||||
transform: Callable[[_S], Tuple[Optional[str], ...]],
|
||||
) -> List[_S]:
|
||||
assert self.query
|
||||
all_results = []
|
@@ -111,7 +111,10 @@ class ConfigureServerForm(Gtk.Box):
|
||||
self.is_networked = is_networked
|
||||
|
||||
content_grid = Gtk.Grid(
|
||||
column_spacing=10, row_spacing=5, margin_left=10, margin_right=10,
|
||||
column_spacing=10,
|
||||
row_spacing=5,
|
||||
margin_left=10,
|
||||
margin_right=10,
|
||||
)
|
||||
advanced_grid = Gtk.Grid(column_spacing=10, row_spacing=10)
|
||||
|
||||
@@ -175,7 +178,8 @@ class ConfigureServerForm(Gtk.Box):
|
||||
|
||||
if cpd.helptext:
|
||||
help_icon = Gtk.Image.new_from_icon_name(
|
||||
"help-about", Gtk.IconSize.BUTTON,
|
||||
"help-about",
|
||||
Gtk.IconSize.BUTTON,
|
||||
)
|
||||
help_icon.get_style_context().add_class("configure-form-help-icon")
|
||||
help_icon.set_tooltip_markup(cpd.helptext)
|
@@ -60,9 +60,7 @@ class FilesystemAdapter(CachingAdapter):
|
||||
def migrate_configuration(config_store: ConfigurationStore):
|
||||
pass
|
||||
|
||||
def __init__(
|
||||
self, config: dict, data_directory: Path, is_cache: bool = False,
|
||||
):
|
||||
def __init__(self, config: dict, data_directory: Path, is_cache: bool = False):
|
||||
self.data_directory = data_directory
|
||||
self.cover_art_dir = self.data_directory.joinpath("cover_art")
|
||||
self.music_dir = self.data_directory.joinpath("music")
|
||||
@@ -311,7 +309,9 @@ class FilesystemAdapter(CachingAdapter):
|
||||
|
||||
def get_song_details(self, song_id: str) -> models.Song:
|
||||
return self._get_object_details(
|
||||
models.Song, song_id, CachingAdapter.CachedDataKey.SONG,
|
||||
models.Song,
|
||||
song_id,
|
||||
CachingAdapter.CachedDataKey.SONG,
|
||||
)
|
||||
|
||||
def get_artists(self, ignore_cache_miss: bool = False) -> Sequence[API.Artist]:
|
||||
@@ -429,7 +429,8 @@ class FilesystemAdapter(CachingAdapter):
|
||||
),
|
||||
)
|
||||
search_result.add_results(
|
||||
"playlists", self.get_playlists(ignore_cache_miss=True),
|
||||
"playlists",
|
||||
self.get_playlists(ignore_cache_miss=True),
|
||||
)
|
||||
return search_result
|
||||
|
||||
@@ -439,7 +440,10 @@ class FilesystemAdapter(CachingAdapter):
|
||||
return hashlib.sha1(bytes(string, "utf8")).hexdigest()
|
||||
|
||||
def ingest_new_data(
|
||||
self, data_key: CachingAdapter.CachedDataKey, param: Optional[str], data: Any,
|
||||
self,
|
||||
data_key: CachingAdapter.CachedDataKey,
|
||||
param: Optional[str],
|
||||
data: Any,
|
||||
):
|
||||
assert self.is_cache, "FilesystemAdapter is not in cache mode!"
|
||||
|
||||
@@ -809,7 +813,9 @@ class FilesystemAdapter(CachingAdapter):
|
||||
)
|
||||
song_data["_cover_art"] = (
|
||||
self._do_ingest_new_data(
|
||||
KEYS.COVER_ART_FILE, api_song.cover_art, data=None,
|
||||
KEYS.COVER_ART_FILE,
|
||||
api_song.cover_art,
|
||||
data=None,
|
||||
)
|
||||
if api_song.cover_art
|
||||
else None
|
||||
@@ -863,7 +869,9 @@ class FilesystemAdapter(CachingAdapter):
|
||||
return return_val if return_val is not None else cache_info
|
||||
|
||||
def _do_invalidate_data(
|
||||
self, data_key: CachingAdapter.CachedDataKey, param: Optional[str],
|
||||
self,
|
||||
data_key: CachingAdapter.CachedDataKey,
|
||||
param: Optional[str],
|
||||
):
|
||||
logging.debug(f"_do_invalidate_data param={param} data_key={data_key}")
|
||||
models.CacheInfo.update({"valid": False}).where(
|
||||
@@ -899,7 +907,8 @@ class FilesystemAdapter(CachingAdapter):
|
||||
):
|
||||
logging.debug(f"_do_delete_data param={param} data_key={data_key}")
|
||||
cache_info = models.CacheInfo.get_or_none(
|
||||
models.CacheInfo.cache_key == data_key, models.CacheInfo.parameter == param,
|
||||
models.CacheInfo.cache_key == data_key,
|
||||
models.CacheInfo.parameter == param,
|
||||
)
|
||||
|
||||
if data_key == KEYS.COVER_ART_FILE:
|
@@ -70,7 +70,10 @@ class SortedManyToManyQuery(ManyToManyQuery):
|
||||
|
||||
class SortedManyToManyFieldAccessor(ManyToManyFieldAccessor):
|
||||
def __get__(
|
||||
self, instance: Model, instance_type: Any = None, force_query: bool = False,
|
||||
self,
|
||||
instance: Model,
|
||||
instance_type: Any = None,
|
||||
force_query: bool = False,
|
||||
):
|
||||
if instance is not None:
|
||||
if not force_query and self.src_fk.backref != "+":
|
Before Width: | Height: | Size: 348 B After Width: | Height: | Size: 348 B |
Before Width: | Height: | Size: 299 B After Width: | Height: | Size: 299 B |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -493,7 +493,8 @@ class AdapterManager:
|
||||
# Everything succeeded.
|
||||
if expected_size_exists:
|
||||
AdapterManager._instance.song_download_progress(
|
||||
id, DownloadProgress(DownloadProgress.Type.DONE),
|
||||
id,
|
||||
DownloadProgress(DownloadProgress.Type.DONE),
|
||||
)
|
||||
except Exception as e:
|
||||
if expected_size_exists and not download_cancelled:
|
||||
@@ -872,7 +873,9 @@ class AdapterManager:
|
||||
# Create a download result.
|
||||
future = AdapterManager._create_download_result(
|
||||
AdapterManager._instance.ground_truth_adapter.get_cover_art_uri(
|
||||
cover_art_id, AdapterManager._get_networked_scheme(), size=size,
|
||||
cover_art_id,
|
||||
AdapterManager._get_networked_scheme(),
|
||||
size=size,
|
||||
),
|
||||
cover_art_id,
|
||||
before_download,
|
||||
@@ -963,7 +966,8 @@ class AdapterManager:
|
||||
):
|
||||
AdapterManager._instance.download_limiter_semaphore.release()
|
||||
AdapterManager._instance.song_download_progress(
|
||||
song_id, DownloadProgress(DownloadProgress.Type.CANCELLED),
|
||||
song_id,
|
||||
DownloadProgress(DownloadProgress.Type.CANCELLED),
|
||||
)
|
||||
return Result("", is_download=True)
|
||||
|
||||
@@ -977,7 +981,8 @@ class AdapterManager:
|
||||
)
|
||||
AdapterManager._instance.download_limiter_semaphore.release()
|
||||
AdapterManager._instance.song_download_progress(
|
||||
song_id, DownloadProgress(DownloadProgress.Type.DONE),
|
||||
song_id,
|
||||
DownloadProgress(DownloadProgress.Type.DONE),
|
||||
)
|
||||
return Result("", is_download=True)
|
||||
except CacheMissError:
|
||||
@@ -1033,7 +1038,8 @@ class AdapterManager:
|
||||
for song_id in song_ids:
|
||||
# Everything succeeded.
|
||||
AdapterManager._instance.song_download_progress(
|
||||
song_id, DownloadProgress(DownloadProgress.Type.QUEUED),
|
||||
song_id,
|
||||
DownloadProgress(DownloadProgress.Type.QUEUED),
|
||||
)
|
||||
|
||||
for song_id in song_ids:
|
||||
@@ -1057,7 +1063,8 @@ class AdapterManager:
|
||||
# Alert the UI that the downloads are cancelled.
|
||||
for song_id in song_ids:
|
||||
AdapterManager._instance.song_download_progress(
|
||||
song_id, DownloadProgress(DownloadProgress.Type.CANCELLED),
|
||||
song_id,
|
||||
DownloadProgress(DownloadProgress.Type.CANCELLED),
|
||||
)
|
||||
|
||||
return Result(do_batch_download_songs, is_download=True, on_cancel=on_cancel)
|
||||
@@ -1070,7 +1077,8 @@ class AdapterManager:
|
||||
)
|
||||
for song_id in song_ids:
|
||||
AdapterManager._instance.song_download_progress(
|
||||
song_id, DownloadProgress(DownloadProgress.Type.CANCELLED),
|
||||
song_id,
|
||||
DownloadProgress(DownloadProgress.Type.CANCELLED),
|
||||
)
|
||||
if AdapterManager._song_download_jobs.get(song_id):
|
||||
AdapterManager._song_download_jobs[song_id].cancel()
|
||||
@@ -1360,8 +1368,10 @@ class AdapterManager:
|
||||
return True
|
||||
|
||||
try:
|
||||
ground_truth_search_results = AdapterManager._instance.ground_truth_adapter.search( # noqa: E501
|
||||
query
|
||||
ground_truth_search_results = (
|
||||
AdapterManager._instance.ground_truth_adapter.search( # noqa: E501
|
||||
query
|
||||
)
|
||||
)
|
||||
search_result.update(ground_truth_search_results)
|
||||
search_callback(search_result)
|
@@ -261,7 +261,9 @@ class SubsonicAdapter(Adapter):
|
||||
|
||||
# Try to ping the server.
|
||||
self._get_json(
|
||||
self._make_url("ping"), timeout=timeout, is_exponential_backoff_ping=True,
|
||||
self._make_url("ping"),
|
||||
timeout=timeout,
|
||||
is_exponential_backoff_ping=True,
|
||||
)
|
||||
|
||||
def on_offline_mode_change(self, offline_mode: bool):
|
||||
@@ -388,7 +390,10 @@ class SubsonicAdapter(Adapter):
|
||||
result = self._get_mock_data()
|
||||
else:
|
||||
result = requests.get(
|
||||
url, params=params, verify=self.verify_cert, timeout=timeout,
|
||||
url,
|
||||
params=params,
|
||||
verify=self.verify_cert,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if result.status_code != 200:
|
||||
@@ -469,10 +474,10 @@ class SubsonicAdapter(Adapter):
|
||||
raise data
|
||||
if hasattr(data, "__next__"):
|
||||
if d := next(data):
|
||||
logging.info("MOCK DATA %s", d)
|
||||
logging.info("MOCK DATA: %s", d)
|
||||
return MockResult(d)
|
||||
|
||||
logging.info("MOCK DATA %s", data)
|
||||
logging.info("MOCK DATA: %s", data)
|
||||
return MockResult(data)
|
||||
|
||||
self._get_mock_data = get_mock_data
|
||||
@@ -490,7 +495,7 @@ class SubsonicAdapter(Adapter):
|
||||
return result
|
||||
|
||||
def create_playlist(
|
||||
self, name: str, songs: Sequence[API.Song] = None,
|
||||
self, name: str, songs: Sequence[API.Song] = None
|
||||
) -> Optional[API.Playlist]:
|
||||
return self._get_json(
|
||||
self._make_url("createPlaylist"),
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -107,7 +107,8 @@ class SublimeMusicApp(Gtk.Application):
|
||||
add_action("go-online", self.on_go_online)
|
||||
add_action("refresh-devices", self.on_refresh_devices)
|
||||
add_action(
|
||||
"refresh-window", lambda *a: self.on_refresh_window(None, {}, True),
|
||||
"refresh-window",
|
||||
lambda *a: self.on_refresh_window(None, {}, True),
|
||||
)
|
||||
add_action("mute-toggle", self.on_mute_toggle)
|
||||
add_action(
|
||||
@@ -413,7 +414,8 @@ class SublimeMusicApp(Gtk.Application):
|
||||
# repeat song IDs.
|
||||
metadatas: Iterable[Any] = [
|
||||
self.dbus_manager.get_mpris_metadata(
|
||||
i, self.app_config.state.play_queue,
|
||||
i,
|
||||
self.app_config.state.play_queue,
|
||||
)
|
||||
for i in range(len(self.app_config.state.play_queue))
|
||||
]
|
||||
@@ -457,7 +459,10 @@ class SublimeMusicApp(Gtk.Application):
|
||||
)
|
||||
|
||||
def get_playlists(
|
||||
index: int, max_count: int, order: str, reverse_order: bool,
|
||||
index: int,
|
||||
max_count: int,
|
||||
order: str,
|
||||
reverse_order: bool,
|
||||
) -> GLib.Variant:
|
||||
playlists_result = AdapterManager.get_playlists()
|
||||
if not playlists_result.data_is_available:
|
||||
@@ -473,12 +478,15 @@ class SublimeMusicApp(Gtk.Application):
|
||||
"Modified": lambda p: p.changed,
|
||||
}
|
||||
playlists.sort(
|
||||
key=sorters.get(order, lambda p: p), reverse=reverse_order,
|
||||
key=sorters.get(order, lambda p: p),
|
||||
reverse=reverse_order,
|
||||
)
|
||||
|
||||
def make_playlist_tuple(p: Playlist) -> GLib.Variant:
|
||||
cover_art_filename = AdapterManager.get_cover_art_uri(
|
||||
p.cover_art, "file", allow_download=False,
|
||||
p.cover_art,
|
||||
"file",
|
||||
allow_download=False,
|
||||
).result()
|
||||
return (f"/playlist/{p.id}", p.name, cover_art_filename or "")
|
||||
|
||||
@@ -570,9 +578,7 @@ class SublimeMusicApp(Gtk.Application):
|
||||
|
||||
# ########## ACTION HANDLERS ########## #
|
||||
@dbus_propagate()
|
||||
def on_refresh_window(
|
||||
self, _, state_updates: Dict[str, Any], force: bool = False,
|
||||
):
|
||||
def on_refresh_window(self, _, state_updates: Dict[str, Any], force: bool = False):
|
||||
if settings := state_updates.get("__settings__"):
|
||||
for k, v in settings.items():
|
||||
setattr(self.app_config, k, v)
|
||||
@@ -879,9 +885,7 @@ class SublimeMusicApp(Gtk.Application):
|
||||
return
|
||||
|
||||
self.app_config.state.current_song_index -= len(before_current)
|
||||
self.play_song(
|
||||
self.app_config.state.current_song_index, reset=True,
|
||||
)
|
||||
self.play_song(self.app_config.state.current_song_index, reset=True)
|
||||
else:
|
||||
self.app_config.state.current_song_index -= len(before_current)
|
||||
self.update_window()
|
||||
@@ -1001,7 +1005,8 @@ class SublimeMusicApp(Gtk.Application):
|
||||
|
||||
# ########## HELPER METHODS ########## #
|
||||
def show_configure_servers_dialog(
|
||||
self, provider_config: Optional[ProviderConfiguration] = None,
|
||||
self,
|
||||
provider_config: Optional[ProviderConfiguration] = None,
|
||||
):
|
||||
"""Show the Connect to Server dialog."""
|
||||
dialog = ConfigureProviderDialog(self.window, provider_config)
|
||||
@@ -1192,7 +1197,8 @@ class SublimeMusicApp(Gtk.Application):
|
||||
if artist := song.artist:
|
||||
notification_lines.append(bleach.clean(artist.name))
|
||||
song_notification = Notify.Notification.new(
|
||||
song.title, "\n".join(notification_lines),
|
||||
song.title,
|
||||
"\n".join(notification_lines),
|
||||
)
|
||||
song_notification.add_action(
|
||||
"clicked",
|
@@ -18,9 +18,7 @@ def encode_path(path: Path) -> str:
|
||||
|
||||
|
||||
dataclasses_json.cfg.global_config.decoders[Path] = Path
|
||||
dataclasses_json.cfg.global_config.decoders[
|
||||
Optional[Path] # type: ignore
|
||||
] = (
|
||||
dataclasses_json.cfg.global_config.decoders[Optional[Path]] = ( # type: ignore
|
||||
lambda p: Path(p) if p else None
|
||||
)
|
||||
|
@@ -137,7 +137,13 @@ class DBusManager:
|
||||
|
||||
return
|
||||
self.do_on_method_call(
|
||||
connection, sender, path, interface, method, params, invocation,
|
||||
connection,
|
||||
sender,
|
||||
path,
|
||||
interface,
|
||||
method,
|
||||
params,
|
||||
invocation,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -153,7 +159,8 @@ class DBusManager:
|
||||
|
||||
if type(value) == dict:
|
||||
return GLib.Variant(
|
||||
"a{sv}", {k: DBusManager.to_variant(v) for k, v in value.items()},
|
||||
"a{sv}",
|
||||
{k: DBusManager.to_variant(v) for k, v in value.items()},
|
||||
)
|
||||
|
||||
variant_type = {list: "as", str: "s", int: "i", float: "d", bool: "b"}.get(
|
||||
@@ -230,7 +237,8 @@ class DBusManager:
|
||||
"Rate": 1.0,
|
||||
"Shuffle": state.shuffle_on,
|
||||
"Metadata": self.get_mpris_metadata(
|
||||
state.current_song_index, state.play_queue,
|
||||
state.current_song_index,
|
||||
state.play_queue,
|
||||
)
|
||||
if state.current_song
|
||||
else {},
|
@@ -67,7 +67,8 @@ class PlayerManager:
|
||||
}
|
||||
|
||||
def change_settings(
|
||||
self, config: Dict[str, Dict[str, Union[Type, Tuple[str, ...]]]],
|
||||
self,
|
||||
config: Dict[str, Dict[str, Union[Type, Tuple[str, ...]]]],
|
||||
):
|
||||
self.config = config
|
||||
for player_type, player in self.players.items():
|
@@ -164,10 +164,12 @@ class AlbumsPanel(Gtk.Box):
|
||||
scrolled_window = Gtk.ScrolledWindow()
|
||||
self.grid = AlbumsGrid()
|
||||
self.grid.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
"song-clicked",
|
||||
lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
self.grid.connect(
|
||||
"refresh-window", lambda _, *args: self.emit("refresh-window", *args),
|
||||
"refresh-window",
|
||||
lambda _, *args: self.emit("refresh-window", *args),
|
||||
)
|
||||
self.grid.connect("cover-clicked", self.on_grid_cover_clicked)
|
||||
self.grid.connect("num-pages-changed", self.on_grid_num_pages_changed)
|
||||
@@ -195,7 +197,9 @@ class AlbumsPanel(Gtk.Box):
|
||||
return combo, store
|
||||
|
||||
def populate_genre_combo(
|
||||
self, app_config: AppConfiguration = None, force: bool = False,
|
||||
self,
|
||||
app_config: AppConfiguration = None,
|
||||
force: bool = False,
|
||||
):
|
||||
if not AdapterManager.can_get_genres():
|
||||
self.updating_query = False
|
||||
@@ -464,13 +468,17 @@ class AlbumsPanel(Gtk.Box):
|
||||
|
||||
def on_grid_cover_clicked(self, grid: Any, id: str):
|
||||
self.emit(
|
||||
"refresh-window", {"selected_album_id": id}, False,
|
||||
"refresh-window",
|
||||
{"selected_album_id": id},
|
||||
False,
|
||||
)
|
||||
|
||||
def on_show_count_dropdown_change(self, combo: Gtk.ComboBox):
|
||||
show_count = int(self.get_id(combo) or 30)
|
||||
self.emit(
|
||||
"refresh-window", {"album_page_size": show_count, "album_page": 0}, False,
|
||||
"refresh-window",
|
||||
{"album_page_size": show_count, "album_page": 0},
|
||||
False,
|
||||
)
|
||||
|
||||
def emit_if_not_updating(self, *args):
|
||||
@@ -483,7 +491,7 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
"""Defines the albums panel."""
|
||||
|
||||
__gsignals__ = {
|
||||
"cover-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,),),
|
||||
"cover-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,)),
|
||||
"refresh-window": (
|
||||
GObject.SignalFlags.RUN_FIRST,
|
||||
GObject.TYPE_NONE,
|
||||
@@ -789,9 +797,7 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
new_items_per_row = min((rect.width // 230), 7)
|
||||
if new_items_per_row != self.items_per_row:
|
||||
self.items_per_row = new_items_per_row
|
||||
self.detail_box_inner.set_size_request(
|
||||
self.items_per_row * 230 - 10, -1,
|
||||
)
|
||||
self.detail_box_inner.set_size_request(self.items_per_row * 230 - 10, -1)
|
||||
|
||||
self.reflow_grids(
|
||||
force_reload_from_master=True,
|
||||
@@ -899,10 +905,14 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
# Just remove everything and re-add all of the items. It's not worth trying
|
||||
# to diff in this case.
|
||||
self.list_store_top.splice(
|
||||
0, len(self.list_store_top), window[:entries_before_fold],
|
||||
0,
|
||||
len(self.list_store_top),
|
||||
window[:entries_before_fold],
|
||||
)
|
||||
self.list_store_bottom.splice(
|
||||
0, len(self.list_store_bottom), window[entries_before_fold:],
|
||||
0,
|
||||
len(self.list_store_bottom),
|
||||
window[entries_before_fold:],
|
||||
)
|
||||
elif selected_index or entries_before_fold != self.page_size:
|
||||
# This case handles when the selection changes and the entries need to be
|
||||
@@ -940,7 +950,8 @@ class AlbumsGrid(Gtk.Overlay):
|
||||
model = self.list_store_top[relative_selected_index]
|
||||
detail_element = AlbumWithSongs(model.album, cover_art_size=300)
|
||||
detail_element.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
"song-clicked",
|
||||
lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
detail_element.connect("song-selected", lambda *a: None)
|
||||
|
@@ -40,10 +40,12 @@ class ArtistsPanel(Gtk.Paned):
|
||||
|
||||
self.artist_detail_panel = ArtistDetailPanel()
|
||||
self.artist_detail_panel.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
"song-clicked",
|
||||
lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
self.artist_detail_panel.connect(
|
||||
"refresh-window", lambda _, *args: self.emit("refresh-window", *args),
|
||||
"refresh-window",
|
||||
lambda _, *args: self.emit("refresh-window", *args),
|
||||
)
|
||||
self.pack2(self.artist_detail_panel, True, False)
|
||||
|
||||
@@ -315,7 +317,8 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
self.album_list_scrolledwindow = Gtk.ScrolledWindow()
|
||||
self.albums_list = AlbumsListWithSongs()
|
||||
self.albums_list.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
"song-clicked",
|
||||
lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
self.album_list_scrolledwindow.add(self.albums_list)
|
||||
self.pack_start(self.album_list_scrolledwindow, True, True, 0)
|
||||
@@ -408,7 +411,9 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
self.play_shuffle_buttons.show_all()
|
||||
|
||||
self.update_artist_artwork(
|
||||
artist.artist_image_url, force=force, order_token=order_token,
|
||||
artist.artist_image_url,
|
||||
force=force,
|
||||
order_token=order_token,
|
||||
)
|
||||
|
||||
for c in self.error_container.get_children():
|
||||
@@ -489,24 +494,31 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
# =========================================================================
|
||||
def on_view_refresh_click(self, *args):
|
||||
self.update_artist_view(
|
||||
self.artist_id, force=True, order_token=self.update_order_token,
|
||||
self.artist_id,
|
||||
force=True,
|
||||
order_token=self.update_order_token,
|
||||
)
|
||||
|
||||
def on_download_all_click(self, _):
|
||||
AdapterManager.batch_download_songs(
|
||||
self.get_artist_song_ids(),
|
||||
before_download=lambda _: self.update_artist_view(
|
||||
self.artist_id, order_token=self.update_order_token,
|
||||
self.artist_id,
|
||||
order_token=self.update_order_token,
|
||||
),
|
||||
on_song_download_complete=lambda _: self.update_artist_view(
|
||||
self.artist_id, order_token=self.update_order_token,
|
||||
self.artist_id,
|
||||
order_token=self.update_order_token,
|
||||
),
|
||||
)
|
||||
|
||||
def on_play_all_clicked(self, _):
|
||||
songs = self.get_artist_song_ids()
|
||||
self.emit(
|
||||
"song-clicked", 0, songs, {"force_shuffle_state": False},
|
||||
"song-clicked",
|
||||
0,
|
||||
songs,
|
||||
{"force_shuffle_state": False},
|
||||
)
|
||||
|
||||
def on_shuffle_all_button(self, _):
|
||||
@@ -633,7 +645,8 @@ class AlbumsListWithSongs(Gtk.Overlay):
|
||||
for album in self.albums:
|
||||
album_with_songs = AlbumWithSongs(album, show_artist_name=False)
|
||||
album_with_songs.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
"song-clicked",
|
||||
lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
album_with_songs.connect("song-selected", self.on_song_selected)
|
||||
album_with_songs.show_all()
|
@@ -37,10 +37,12 @@ class BrowsePanel(Gtk.Overlay):
|
||||
|
||||
self.root_directory_listing = ListAndDrilldown()
|
||||
self.root_directory_listing.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
"song-clicked",
|
||||
lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
self.root_directory_listing.connect(
|
||||
"refresh-window", lambda _, *args: self.emit("refresh-window", *args),
|
||||
"refresh-window",
|
||||
lambda _, *args: self.emit("refresh-window", *args),
|
||||
)
|
||||
window_box.add(self.root_directory_listing)
|
||||
|
||||
@@ -88,7 +90,8 @@ class BrowsePanel(Gtk.Overlay):
|
||||
while current_dir_id:
|
||||
try:
|
||||
directory = AdapterManager.get_directory(
|
||||
current_dir_id, before_download=self.spinner.show,
|
||||
current_dir_id,
|
||||
before_download=self.spinner.show,
|
||||
).result()
|
||||
except CacheMissError as e:
|
||||
directory = cast(API.Directory, e.partial_data)
|
||||
@@ -128,10 +131,12 @@ class ListAndDrilldown(Gtk.Paned):
|
||||
|
||||
self.list = MusicDirectoryList()
|
||||
self.list.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
"song-clicked",
|
||||
lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
self.list.connect(
|
||||
"refresh-window", lambda _, *args: self.emit("refresh-window", *args),
|
||||
"refresh-window",
|
||||
lambda _, *args: self.emit("refresh-window", *args),
|
||||
)
|
||||
self.pack1(self.list, False, False)
|
||||
|
||||
@@ -163,7 +168,8 @@ class ListAndDrilldown(Gtk.Paned):
|
||||
if len(children) == 0:
|
||||
drilldown = ListAndDrilldown()
|
||||
drilldown.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
"song-clicked",
|
||||
lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
drilldown.connect(
|
||||
"refresh-window",
|
||||
@@ -277,7 +283,9 @@ class MusicDirectoryList(Gtk.Box):
|
||||
self.directory_id = directory_id or self.directory_id
|
||||
self.selected_id = selected_id or self.selected_id
|
||||
self.update_store(
|
||||
self.directory_id, force=force, order_token=self.update_order_token,
|
||||
self.directory_id,
|
||||
force=force,
|
||||
order_token=self.update_order_token,
|
||||
)
|
||||
|
||||
if app_config:
|
||||
@@ -428,7 +436,8 @@ class MusicDirectoryList(Gtk.Box):
|
||||
# ==================================================================================
|
||||
def create_row(self, model: DrilldownElement) -> Gtk.ListBoxRow:
|
||||
row = Gtk.ListBoxRow(
|
||||
action_name="app.browse-to", action_target=GLib.Variant("s", model.id),
|
||||
action_name="app.browse-to",
|
||||
action_target=GLib.Variant("s", model.id),
|
||||
)
|
||||
rowbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
rowbox.add(
|
||||
@@ -461,7 +470,7 @@ class MusicDirectoryList(Gtk.Box):
|
||||
{},
|
||||
)
|
||||
|
||||
def on_song_button_press(self, tree: Gtk.TreeView, event: Gdk.EventButton,) -> bool:
|
||||
def on_song_button_press(self, tree: Gtk.TreeView, event: Gdk.EventButton) -> bool:
|
||||
if event.button == 3: # Right click
|
||||
clicked_path = tree.get_path_at_pos(event.x, event.y)
|
||||
if not clicked_path:
|
@@ -15,7 +15,7 @@ from .spinner_image import SpinnerImage
|
||||
|
||||
class AlbumWithSongs(Gtk.Box):
|
||||
__gsignals__ = {
|
||||
"song-selected": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (),),
|
||||
"song-selected": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()),
|
||||
"song-clicked": (
|
||||
GObject.SignalFlags.RUN_FIRST,
|
||||
GObject.TYPE_NONE,
|
||||
@@ -122,7 +122,9 @@ class AlbumWithSongs(Gtk.Box):
|
||||
|
||||
album_details.add(
|
||||
Gtk.Label(
|
||||
label=util.dot_join(*stats), halign=Gtk.Align.START, margin_left=10,
|
||||
label=util.dot_join(*stats),
|
||||
halign=Gtk.Align.START,
|
||||
margin_left=10,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -235,7 +237,10 @@ class AlbumWithSongs(Gtk.Box):
|
||||
def play_btn_clicked(self, btn: Any):
|
||||
song_ids = [x[-1] for x in self.album_song_store]
|
||||
self.emit(
|
||||
"song-clicked", 0, song_ids, {"force_shuffle_state": False},
|
||||
"song-clicked",
|
||||
0,
|
||||
song_ids,
|
||||
{"force_shuffle_state": False},
|
||||
)
|
||||
|
||||
def shuffle_btn_clicked(self, btn: Any):
|
@@ -35,7 +35,7 @@ class SpinnerImage(Gtk.Overlay):
|
||||
self.filename = filename
|
||||
if self.image_size is not None and filename:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||
filename, self.image_size, self.image_size, True,
|
||||
filename, self.image_size, self.image_size, True
|
||||
)
|
||||
self.image.set_from_pixbuf(pixbuf)
|
||||
else:
|
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 402 B |
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 402 B |
Before Width: | Height: | Size: 372 B After Width: | Height: | Size: 372 B |
Before Width: | Height: | Size: 276 B After Width: | Height: | Size: 276 B |
Before Width: | Height: | Size: 391 B After Width: | Height: | Size: 391 B |
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 392 B |
Before Width: | Height: | Size: 388 B After Width: | Height: | Size: 388 B |
Before Width: | Height: | Size: 157 B After Width: | Height: | Size: 157 B |
Before Width: | Height: | Size: 155 B After Width: | Height: | Size: 155 B |
Before Width: | Height: | Size: 157 B After Width: | Height: | Size: 157 B |
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 903 B |
Before Width: | Height: | Size: 213 B After Width: | Height: | Size: 213 B |
@@ -24,14 +24,14 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
GObject.TYPE_NONE,
|
||||
(int, object, object),
|
||||
),
|
||||
"songs-removed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,),),
|
||||
"songs-removed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,)),
|
||||
"refresh-window": (
|
||||
GObject.SignalFlags.RUN_FIRST,
|
||||
GObject.TYPE_NONE,
|
||||
(object, bool),
|
||||
),
|
||||
"notification-closed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (),),
|
||||
"go-to": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str, str),),
|
||||
"notification-closed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ()),
|
||||
"go-to": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str, str)),
|
||||
}
|
||||
|
||||
_updating_settings: bool = False
|
||||
@@ -100,7 +100,8 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
"songs-removed", lambda _, *a: self.emit("songs-removed", *a)
|
||||
)
|
||||
self.player_controls.connect(
|
||||
"refresh-window", lambda _, *args: self.emit("refresh-window", *args),
|
||||
"refresh-window",
|
||||
lambda _, *args: self.emit("refresh-window", *args),
|
||||
)
|
||||
flowbox.pack_start(self.player_controls, False, True, 0)
|
||||
|
||||
@@ -167,7 +168,8 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
f"{icon_basename}-{icon_status}-symbolic"
|
||||
)
|
||||
self.connection_status_icon.set_from_icon_name(
|
||||
f"server-{icon_status}-symbolic", Gtk.IconSize.BUTTON,
|
||||
f"server-{icon_status}-symbolic",
|
||||
Gtk.IconSize.BUTTON,
|
||||
)
|
||||
self.connection_status_label.set_text(status_label)
|
||||
self.connected_status_box.show_all()
|
||||
@@ -194,7 +196,10 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
for provider in sorted(other_providers, key=lambda p: p.name.lower()):
|
||||
self.provider_options_box.pack_start(
|
||||
self._create_switch_provider_button(provider), False, True, 0,
|
||||
self._create_switch_provider_button(provider),
|
||||
False,
|
||||
True,
|
||||
0,
|
||||
)
|
||||
|
||||
self.provider_options_box.show_all()
|
||||
@@ -489,17 +494,21 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
def _on_retry_all_clicked(self, _):
|
||||
AdapterManager.batch_download_songs(
|
||||
self._failed_downloads, lambda _: None, lambda _: None,
|
||||
self._failed_downloads,
|
||||
lambda _: None,
|
||||
lambda _: None,
|
||||
)
|
||||
|
||||
def _create_stack(self, **kwargs: Gtk.Widget) -> Gtk.Stack:
|
||||
stack = Gtk.Stack()
|
||||
for name, child in kwargs.items():
|
||||
child.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
"song-clicked",
|
||||
lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
child.connect(
|
||||
"refresh-window", lambda _, *args: self.emit("refresh-window", *args),
|
||||
"refresh-window",
|
||||
lambda _, *args: self.emit("refresh-window", *args),
|
||||
)
|
||||
stack.add_titled(child, name.lower(), name)
|
||||
return stack
|
||||
@@ -671,7 +680,8 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
current_downloads_header = Gtk.Box()
|
||||
current_downloads_header.add(
|
||||
current_downloads_label := Gtk.Label(
|
||||
label="Current Downloads", name="menu-header",
|
||||
label="Current Downloads",
|
||||
name="menu-header",
|
||||
)
|
||||
)
|
||||
current_downloads_label.get_style_context().add_class("menu-label")
|
||||
@@ -767,7 +777,8 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
vbox.add(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
|
||||
|
||||
music_provider_button = self._create_model_button(
|
||||
"Switch Music Provider", menu_name="switch-provider",
|
||||
"Switch Music Provider",
|
||||
menu_name="switch-provider",
|
||||
)
|
||||
vbox.add(music_provider_button)
|
||||
|
||||
@@ -858,7 +869,8 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
self.search_popup = Gtk.PopoverMenu(modal=False)
|
||||
|
||||
results_scrollbox = Gtk.ScrolledWindow(
|
||||
min_content_width=500, min_content_height=700,
|
||||
min_content_width=500,
|
||||
min_content_height=700,
|
||||
)
|
||||
|
||||
def make_search_result_header(text: str) -> Gtk.Label:
|
||||
@@ -867,7 +879,8 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
return label
|
||||
|
||||
search_results_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, name="search-results",
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
name="search-results",
|
||||
)
|
||||
self.search_results_loading = Gtk.Spinner(active=False, name="search-spinner")
|
||||
search_results_box.add(self.search_results_loading)
|
||||
@@ -1155,8 +1168,8 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
class DownloadStatusBox(Gtk.Box):
|
||||
__gsignals__ = {
|
||||
"cancel-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str,),),
|
||||
"retry-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str,),),
|
||||
"cancel-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str,)),
|
||||
"retry-clicked": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str,)),
|
||||
}
|
||||
|
||||
def __init__(self, song_id: str):
|
@@ -21,15 +21,15 @@ class PlayerControls(Gtk.ActionBar):
|
||||
"""
|
||||
|
||||
__gsignals__ = {
|
||||
"song-scrub": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (float,),),
|
||||
"volume-change": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (float,),),
|
||||
"device-update": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str,),),
|
||||
"song-scrub": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (float,)),
|
||||
"volume-change": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (float,)),
|
||||
"device-update": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (str,)),
|
||||
"song-clicked": (
|
||||
GObject.SignalFlags.RUN_FIRST,
|
||||
GObject.TYPE_NONE,
|
||||
(int, object, object),
|
||||
),
|
||||
"songs-removed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,),),
|
||||
"songs-removed": (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, (object,)),
|
||||
"refresh-window": (
|
||||
GObject.SignalFlags.RUN_FIRST,
|
||||
GObject.TYPE_NONE,
|
||||
@@ -211,6 +211,11 @@ class PlayerControls(Gtk.ActionBar):
|
||||
self.current_play_queue = app_config.state.play_queue
|
||||
self.current_playing_index = app_config.state.current_song_index
|
||||
|
||||
print("DIFF STORE")
|
||||
from time import time
|
||||
|
||||
s = time()
|
||||
|
||||
# Set the Play Queue button popup.
|
||||
play_queue_len = len(app_config.state.play_queue)
|
||||
if play_queue_len == 0:
|
||||
@@ -237,12 +242,16 @@ class PlayerControls(Gtk.ActionBar):
|
||||
return f"<b>{title}</b>\n{util.dot_join(album, artist)}"
|
||||
|
||||
def make_idle_index_capturing_function(
|
||||
idx: int, order_tok: int, fn: Callable[[int, int, Any], None],
|
||||
idx: int,
|
||||
order_tok: int,
|
||||
fn: Callable[[int, int, Any], None],
|
||||
) -> Callable[[Result], None]:
|
||||
return lambda f: GLib.idle_add(fn, idx, order_tok, f.result())
|
||||
|
||||
def on_cover_art_future_done(
|
||||
idx: int, order_token: int, cover_art_filename: str,
|
||||
idx: int,
|
||||
order_token: int,
|
||||
cover_art_filename: str,
|
||||
):
|
||||
if order_token != self.play_queue_update_order_token:
|
||||
return
|
||||
@@ -264,9 +273,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
# The cover art is already cached.
|
||||
return cover_art_result.result()
|
||||
|
||||
def on_song_details_future_done(
|
||||
idx: int, order_token: int, song_details: Song,
|
||||
):
|
||||
def on_song_details_future_done(idx: int, order_token: int, song_details: Song):
|
||||
if order_token != self.play_queue_update_order_token:
|
||||
return
|
||||
|
||||
@@ -279,10 +286,13 @@ class PlayerControls(Gtk.ActionBar):
|
||||
if filename:
|
||||
self.play_queue_store[idx][1] = filename
|
||||
|
||||
print("A", time() - s)
|
||||
current_play_queue = [x[-1] for x in self.play_queue_store]
|
||||
if app_config.state.play_queue != current_play_queue:
|
||||
self.play_queue_update_order_token += 1
|
||||
|
||||
print("B", time() - s)
|
||||
ohea = {1: 0.0, 2: 0.0, 3: 0.0}
|
||||
song_details_results = []
|
||||
for i, (song_id, cached_status) in enumerate(
|
||||
zip(
|
||||
@@ -290,7 +300,9 @@ class PlayerControls(Gtk.ActionBar):
|
||||
AdapterManager.get_cached_statuses(app_config.state.play_queue),
|
||||
)
|
||||
):
|
||||
f = time()
|
||||
song_details_result = AdapterManager.get_song_details(song_id)
|
||||
ohea[1] += time() - f
|
||||
|
||||
cover_art_filename = ""
|
||||
label = "\n"
|
||||
@@ -307,6 +319,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
cover_art_filename = filename
|
||||
else:
|
||||
song_details_results.append((i, song_details_result))
|
||||
ohea[2] += time() - f
|
||||
|
||||
new_store.append(
|
||||
[
|
||||
@@ -321,8 +334,16 @@ class PlayerControls(Gtk.ActionBar):
|
||||
song_id,
|
||||
]
|
||||
)
|
||||
ohea[3] += time() - f
|
||||
print(
|
||||
"ohea",
|
||||
ohea,
|
||||
list(map(lambda x: x / len(app_config.state.play_queue), ohea.values())),
|
||||
)
|
||||
print("C", time() - s)
|
||||
|
||||
util.diff_song_store(self.play_queue_store, new_store)
|
||||
print("FOO", time() - s)
|
||||
|
||||
# Do this after the diff to avoid race conditions.
|
||||
for idx, song_details_result in song_details_results:
|
||||
@@ -540,7 +561,8 @@ class PlayerControls(Gtk.ActionBar):
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
self.album_art = SpinnerImage(
|
||||
image_name="player-controls-album-artwork", image_size=70,
|
||||
image_name="player-controls-album-artwork",
|
||||
image_size=70,
|
||||
)
|
||||
box.pack_start(self.album_art, False, False, 0)
|
||||
|
||||
@@ -672,12 +694,16 @@ class PlayerControls(Gtk.ActionBar):
|
||||
self.device_popover.set_relative_to(self.device_button)
|
||||
|
||||
device_popover_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, name="device-popover-box",
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
name="device-popover-box",
|
||||
)
|
||||
device_popover_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||
|
||||
self.popover_label = Gtk.Label(
|
||||
label="<b>Devices</b>", use_markup=True, halign=Gtk.Align.START, margin=5,
|
||||
label="<b>Devices</b>",
|
||||
use_markup=True,
|
||||
halign=Gtk.Align.START,
|
||||
margin=5,
|
||||
)
|
||||
device_popover_header.add(self.popover_label)
|
||||
|
||||
@@ -729,7 +755,8 @@ class PlayerControls(Gtk.ActionBar):
|
||||
|
||||
play_queue_loading_overlay = Gtk.Overlay()
|
||||
play_queue_scrollbox = Gtk.ScrolledWindow(
|
||||
min_content_height=600, min_content_width=400,
|
||||
min_content_height=600,
|
||||
min_content_width=400,
|
||||
)
|
||||
|
||||
self.play_queue_store = Gtk.ListStore(
|
||||
@@ -740,7 +767,9 @@ class PlayerControls(Gtk.ActionBar):
|
||||
str, # song ID
|
||||
)
|
||||
self.play_queue_list = Gtk.TreeView(
|
||||
model=self.play_queue_store, reorderable=True, headers_visible=False,
|
||||
model=self.play_queue_store,
|
||||
reorderable=True,
|
||||
headers_visible=False,
|
||||
)
|
||||
selection = self.play_queue_list.get_selection()
|
||||
selection.set_mode(Gtk.SelectionMode.MULTIPLE)
|
@@ -104,10 +104,12 @@ class PlaylistsPanel(Gtk.Paned):
|
||||
|
||||
self.playlist_detail_panel = PlaylistDetailPanel()
|
||||
self.playlist_detail_panel.connect(
|
||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||
"song-clicked",
|
||||
lambda _, *args: self.emit("song-clicked", *args),
|
||||
)
|
||||
self.playlist_detail_panel.connect(
|
||||
"refresh-window", lambda _, *args: self.emit("refresh-window", *args),
|
||||
"refresh-window",
|
||||
lambda _, *args: self.emit("refresh-window", *args),
|
||||
)
|
||||
self.pack2(self.playlist_detail_panel, True, False)
|
||||
|
||||
@@ -158,7 +160,7 @@ class PlaylistList(Gtk.Box):
|
||||
|
||||
loading_new_playlist = Gtk.ListBox()
|
||||
|
||||
self.loading_indicator = Gtk.ListBoxRow(activatable=False, selectable=False,)
|
||||
self.loading_indicator = Gtk.ListBoxRow(activatable=False, selectable=False)
|
||||
loading_spinner = Gtk.Spinner(name="playlist-list-spinner", active=True)
|
||||
self.loading_indicator.add(loading_spinner)
|
||||
loading_new_playlist.add(self.loading_indicator)
|
||||
@@ -364,13 +366,17 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
)
|
||||
|
||||
self.play_all_button = IconButton(
|
||||
"media-playback-start-symbolic", label="Play All", relief=True,
|
||||
"media-playback-start-symbolic",
|
||||
label="Play All",
|
||||
relief=True,
|
||||
)
|
||||
self.play_all_button.connect("clicked", self.on_play_all_clicked)
|
||||
self.play_shuffle_buttons.pack_start(self.play_all_button, False, False, 0)
|
||||
|
||||
self.shuffle_all_button = IconButton(
|
||||
"media-playlist-shuffle-symbolic", label="Shuffle All", relief=True,
|
||||
"media-playlist-shuffle-symbolic",
|
||||
label="Shuffle All",
|
||||
relief=True,
|
||||
)
|
||||
self.shuffle_all_button.connect("clicked", self.on_shuffle_all_button)
|
||||
self.play_shuffle_buttons.pack_start(self.shuffle_all_button, False, False, 5)
|
||||
@@ -908,7 +914,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
self.playlist_artwork.set_loading(False)
|
||||
self.playlist_view_loading_box.hide()
|
||||
|
||||
def make_label(self, text: str = None, name: str = None, **params,) -> Gtk.Label:
|
||||
def make_label(self, text: str = None, name: str = None, **params) -> Gtk.Label:
|
||||
return Gtk.Label(
|
||||
label=text,
|
||||
name=name,
|
||||
@@ -919,7 +925,10 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
@util.async_callback(AdapterManager.get_playlist_details)
|
||||
def _update_playlist_order(
|
||||
self, playlist: API.Playlist, app_config: AppConfiguration, **kwargs,
|
||||
self,
|
||||
playlist: API.Playlist,
|
||||
app_config: AppConfiguration,
|
||||
**kwargs,
|
||||
):
|
||||
self.playlist_view_loading_box.show_all()
|
||||
update_playlist_future = AdapterManager.update_playlist(
|
@@ -87,7 +87,9 @@ class UIState:
|
||||
self.name = "Rock"
|
||||
|
||||
current_album_search_query: AlbumSearchQuery = AlbumSearchQuery(
|
||||
AlbumSearchQuery.Type.RANDOM, genre=_DefaultGenre(), year_range=(2010, 2020),
|
||||
AlbumSearchQuery.Type.RANDOM,
|
||||
genre=_DefaultGenre(),
|
||||
year_range=(2010, 2020),
|
||||
)
|
||||
|
||||
active_playlist_id: Optional[str] = None
|
@@ -45,7 +45,7 @@ def format_song_duration(duration_secs: Union[int, timedelta, None]) -> str:
|
||||
return f"{duration_secs // 60}:{duration_secs % 60:02}"
|
||||
|
||||
|
||||
def pluralize(string: str, number: int, pluralized_form: str = None,) -> str:
|
||||
def pluralize(string: str, number: int, pluralized_form: str = None) -> str:
|
||||
"""
|
||||
Pluralize the given string given the count as a number.
|
||||
|
||||
@@ -205,7 +205,8 @@ def show_song_popover(
|
||||
def do_on_remove_downloads_click(_: Any):
|
||||
AdapterManager.cancel_download_songs(song_ids)
|
||||
AdapterManager.batch_delete_cached_songs(
|
||||
song_ids, on_song_delete=on_download_state_change,
|
||||
song_ids,
|
||||
on_song_delete=on_download_state_change,
|
||||
)
|
||||
on_remove_downloads_click()
|
||||
|
||||
@@ -438,7 +439,10 @@ def async_callback(
|
||||
GLib.idle_add(fn)
|
||||
|
||||
result: Result = future_fn(
|
||||
*args, before_download=on_before_download, force=force, **kwargs,
|
||||
*args,
|
||||
before_download=on_before_download,
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
result.add_done_callback(
|
||||
functools.partial(future_callback, result.data_is_available)
|
@@ -170,7 +170,8 @@ def test_search_result_update():
|
||||
|
||||
search_results2 = SearchResult(query="foo")
|
||||
search_results2.add_results(
|
||||
"artists", [SubsonicAPI.ArtistAndArtistInfo(id="3", name="foo2")],
|
||||
"artists",
|
||||
[SubsonicAPI.ArtistAndArtistInfo(id="3", name="foo2")],
|
||||
)
|
||||
|
||||
search_results1.update(search_results2)
|
||||
|
@@ -90,7 +90,8 @@ def cache_adapter(tmp_path: Path):
|
||||
|
||||
|
||||
def mock_data_files(
|
||||
request_name: str, mode: str = "r",
|
||||
request_name: str,
|
||||
mode: str = "r",
|
||||
) -> Generator[Tuple[Path, Any], None, None]:
|
||||
"""
|
||||
Yields all of the files in the mock_data directory that start with ``request_name``.
|
||||
@@ -272,7 +273,9 @@ def test_caching_get_playlist_then_details(cache_adapter: FilesystemAdapter):
|
||||
|
||||
# Simulate getting playlist details for id=1, then id=2
|
||||
cache_adapter.ingest_new_data(
|
||||
KEYS.PLAYLIST_DETAILS, "1", SubsonicAPI.Playlist("1", "test1"),
|
||||
KEYS.PLAYLIST_DETAILS,
|
||||
"1",
|
||||
SubsonicAPI.Playlist("1", "test1"),
|
||||
)
|
||||
|
||||
cache_adapter.ingest_new_data(
|
||||
@@ -308,7 +311,9 @@ def test_invalidate_playlist(cache_adapter: FilesystemAdapter):
|
||||
[SubsonicAPI.Playlist("1", "test1"), SubsonicAPI.Playlist("2", "test2")],
|
||||
)
|
||||
cache_adapter.ingest_new_data(
|
||||
KEYS.COVER_ART_FILE, "pl_test1", MOCK_ALBUM_ART,
|
||||
KEYS.COVER_ART_FILE,
|
||||
"pl_test1",
|
||||
MOCK_ALBUM_ART,
|
||||
)
|
||||
cache_adapter.ingest_new_data(
|
||||
KEYS.PLAYLIST_DETAILS,
|
||||
@@ -316,7 +321,9 @@ def test_invalidate_playlist(cache_adapter: FilesystemAdapter):
|
||||
SubsonicAPI.Playlist("2", "test2", cover_art="pl_2", songs=[]),
|
||||
)
|
||||
cache_adapter.ingest_new_data(
|
||||
KEYS.COVER_ART_FILE, "pl_2", MOCK_ALBUM_ART2,
|
||||
KEYS.COVER_ART_FILE,
|
||||
"pl_2",
|
||||
MOCK_ALBUM_ART2,
|
||||
)
|
||||
|
||||
stale_uri_1 = cache_adapter.get_cover_art_uri("pl_test1", "file", size=300)
|
||||
@@ -362,7 +369,9 @@ def test_invalidate_song_file(cache_adapter: FilesystemAdapter):
|
||||
cache_adapter.ingest_new_data(KEYS.SONG, "2", MOCK_SUBSONIC_SONGS[0])
|
||||
cache_adapter.ingest_new_data(KEYS.SONG, "1", MOCK_SUBSONIC_SONGS[1])
|
||||
cache_adapter.ingest_new_data(
|
||||
KEYS.COVER_ART_FILE, "s1", MOCK_ALBUM_ART,
|
||||
KEYS.COVER_ART_FILE,
|
||||
"s1",
|
||||
MOCK_ALBUM_ART,
|
||||
)
|
||||
cache_adapter.ingest_new_data(KEYS.SONG_FILE, "1", (None, MOCK_SONG_FILE, None))
|
||||
cache_adapter.ingest_new_data(KEYS.SONG_FILE, "2", (None, MOCK_SONG_FILE2, None))
|
||||
@@ -430,7 +439,9 @@ def test_delete_playlists(cache_adapter: FilesystemAdapter):
|
||||
SubsonicAPI.Playlist("2", "test1", cover_art="pl_2", songs=[]),
|
||||
)
|
||||
cache_adapter.ingest_new_data(
|
||||
KEYS.COVER_ART_FILE, "pl_1", MOCK_ALBUM_ART,
|
||||
KEYS.COVER_ART_FILE,
|
||||
"pl_1",
|
||||
MOCK_ALBUM_ART,
|
||||
)
|
||||
|
||||
# Deleting a playlist should get rid of it entirely.
|
||||
@@ -451,7 +462,8 @@ def test_delete_playlists(cache_adapter: FilesystemAdapter):
|
||||
|
||||
# Even if the cover art failed to be deleted, it should cache miss.
|
||||
shutil.copy(
|
||||
MOCK_ALBUM_ART, str(cache_adapter.cover_art_dir.joinpath(MOCK_ALBUM_ART_HASH)),
|
||||
MOCK_ALBUM_ART,
|
||||
str(cache_adapter.cover_art_dir.joinpath(MOCK_ALBUM_ART_HASH)),
|
||||
)
|
||||
try:
|
||||
cache_adapter.get_cover_art_uri("pl_1", "file", size=300)
|
||||
@@ -464,7 +476,9 @@ def test_delete_song_data(cache_adapter: FilesystemAdapter):
|
||||
cache_adapter.ingest_new_data(KEYS.SONG, "1", MOCK_SUBSONIC_SONGS[1])
|
||||
cache_adapter.ingest_new_data(KEYS.SONG_FILE, "1", (None, MOCK_SONG_FILE, None))
|
||||
cache_adapter.ingest_new_data(
|
||||
KEYS.COVER_ART_FILE, "s1", MOCK_ALBUM_ART,
|
||||
KEYS.COVER_ART_FILE,
|
||||
"s1",
|
||||
MOCK_ALBUM_ART,
|
||||
)
|
||||
|
||||
music_file_path = cache_adapter.get_song_file_uri("1", "file")
|
||||
@@ -747,14 +761,18 @@ def test_caching_get_artist(cache_adapter: FilesystemAdapter):
|
||||
)
|
||||
|
||||
artist = cache_adapter.get_artist("1")
|
||||
assert artist.artist_image_url and (
|
||||
artist.id,
|
||||
artist.name,
|
||||
artist.album_count,
|
||||
artist.artist_image_url,
|
||||
artist.biography,
|
||||
artist.music_brainz_id,
|
||||
) == ("1", "Bar", 1, "image", "this is a bio", "mbid")
|
||||
assert (
|
||||
artist.artist_image_url
|
||||
and (
|
||||
artist.id,
|
||||
artist.name,
|
||||
artist.album_count,
|
||||
artist.artist_image_url,
|
||||
artist.biography,
|
||||
artist.music_brainz_id,
|
||||
)
|
||||
== ("1", "Bar", 1, "image", "this is a bio", "mbid")
|
||||
)
|
||||
assert artist.similar_artists == [
|
||||
SubsonicAPI.ArtistAndArtistInfo(id="A", name="B"),
|
||||
SubsonicAPI.ArtistAndArtistInfo(id="C", name="D"),
|
||||
@@ -787,14 +805,18 @@ def test_caching_get_artist(cache_adapter: FilesystemAdapter):
|
||||
)
|
||||
|
||||
artist = cache_adapter.get_artist("1")
|
||||
assert artist.artist_image_url and (
|
||||
artist.id,
|
||||
artist.name,
|
||||
artist.album_count,
|
||||
artist.artist_image_url,
|
||||
artist.biography,
|
||||
artist.music_brainz_id,
|
||||
) == ("1", "Foo", 2, "image2", "this is a bio2", "mbid2")
|
||||
assert (
|
||||
artist.artist_image_url
|
||||
and (
|
||||
artist.id,
|
||||
artist.name,
|
||||
artist.album_count,
|
||||
artist.artist_image_url,
|
||||
artist.biography,
|
||||
artist.music_brainz_id,
|
||||
)
|
||||
== ("1", "Foo", 2, "image2", "this is a bio2", "mbid2")
|
||||
)
|
||||
assert artist.similar_artists == [
|
||||
SubsonicAPI.ArtistAndArtistInfo(id="A", name="B"),
|
||||
SubsonicAPI.ArtistAndArtistInfo(id="E", name="F"),
|
||||
@@ -837,7 +859,14 @@ def test_caching_get_album(cache_adapter: FilesystemAdapter):
|
||||
album.song_count,
|
||||
album.year,
|
||||
album.play_count,
|
||||
) == ("a1", "foo", "c", 2, 2020, 20,)
|
||||
) == (
|
||||
"a1",
|
||||
"foo",
|
||||
"c",
|
||||
2,
|
||||
2020,
|
||||
20,
|
||||
)
|
||||
assert album.artist
|
||||
assert (album.artist.id, album.artist.name) == ("art1", "cool")
|
||||
assert album.songs
|
||||
|
@@ -41,7 +41,10 @@ def test_song_list_column():
|
||||
def test_spinner_image():
|
||||
initial_size = 300
|
||||
image = common.SpinnerImage(
|
||||
loading=False, image_name="test", spinner_name="ohea", image_size=initial_size,
|
||||
loading=False,
|
||||
image_name="test",
|
||||
spinner_name="ohea",
|
||||
image_size=initial_size,
|
||||
)
|
||||
image.set_from_file(None)
|
||||
assert image.image.get_pixbuf() is None
|
||||
|