Merge branch 'srht-builds' into 218-python3.8-flatpak

This commit is contained in:
Sumner Evans
2020-09-19 10:54:09 -06:00
94 changed files with 1641 additions and 159 deletions

52
.builds/build.yml Normal file
View 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}"

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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 = '''
(

View File

@@ -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",

View File

@@ -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.

View File

@@ -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 = []

View File

@@ -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)

View File

@@ -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:

View 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 != "+":

View File

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 348 B

View File

Before

Width:  |  Height:  |  Size: 299 B

After

Width:  |  Height:  |  Size: 299 B

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -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)

View File

@@ -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"),

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -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",

View File

@@ -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
)

View File

@@ -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 {},

View File

@@ -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():

View File

@@ -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)

View File

@@ -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()

View File

@@ -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:

View File

@@ -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):

View File

@@ -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:

View File

Before

Width:  |  Height:  |  Size: 325 B

After

Width:  |  Height:  |  Size: 325 B

View File

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 402 B

View File

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 402 B

View File

Before

Width:  |  Height:  |  Size: 372 B

After

Width:  |  Height:  |  Size: 372 B

View File

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 276 B

View File

Before

Width:  |  Height:  |  Size: 391 B

After

Width:  |  Height:  |  Size: 391 B

View File

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 392 B

View File

Before

Width:  |  Height:  |  Size: 388 B

After

Width:  |  Height:  |  Size: 388 B

View File

Before

Width:  |  Height:  |  Size: 157 B

After

Width:  |  Height:  |  Size: 157 B

View File

Before

Width:  |  Height:  |  Size: 155 B

After

Width:  |  Height:  |  Size: 155 B

View File

Before

Width:  |  Height:  |  Size: 157 B

After

Width:  |  Height:  |  Size: 157 B

View File

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 903 B

View File

Before

Width:  |  Height:  |  Size: 213 B

After

Width:  |  Height:  |  Size: 213 B

View File

@@ -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):

View File

@@ -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)

View File

@@ -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(

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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