Skip to content
Commits on Source (50)
Overview of changes in 42.1
===========================
• Make shuffle shuffle again
• Fix time display in RTL languages
• No longer mismatch art on scrolling
• Fix async queue block on fresh art retrieval
Bugs fixed:
Rework ArtStack to handle cycling widgets better (#500)
Shuffle broken in Music 42 (#515)
Time is reversed in RTL (#509)
Thanks to our contributors this release:
Marinus Schraal
Yosef Or Boczko
Updated or added translations:
Latvian
Dutch
Catalan
Icelandic
French
Chinese (China)
Overview of changes in 42.0
===========================
......
......@@ -28,6 +28,16 @@
</screenshot>
</screenshots>
<releases>
<release version="42.1" date="2022-04-25">
<description>
<ul>
<li>Make shuffle shuffle again</li>
<li>Fix time display in RTL languages</li>
<li>No longer mismatch art on scrolling</li>
<li>Fix async queue block on fresh art retrieval</li>
</ul>
</description>
</release>
<release version="42.0" date="2022-03-20">
<description>
<p>
......
......@@ -13,7 +13,7 @@
<property name="focusable">False</property>
<property name="margin-bottom">4</property>
<child>
<object class="ArtStack" id="_art_stack">
<object class="GtkImage" id="_cover_image">
<property name="focusable">False</property>
<property name="vexpand">True</property>
<property name="valign">end</property>
......
......@@ -10,10 +10,9 @@
<object class="GtkOverlay">
<property name="margin-bottom">6</property>
<child>
<object class="ArtStack" id="_art_stack">
<property name="focusable">False</property>
<object class="GtkImage" id="_cover_image">
<property name="vexpand">True</property>
<property name="valign">end</property>
<property name="valign">center</property>
<property name="halign">center</property>
</object>
</child>
......
......@@ -16,11 +16,9 @@
<property name="hexpand">True</property>
<property name="spacing">32</property>
<child>
<object class="ArtStack" id="_art_stack">
<object class="GtkImage" id="_cover_image">
<property name="halign">center</property>
<property name="valign">start</property>
<property name="hhomogeneous">False</property>
<property name="vhomogeneous">False</property>
<property name="valign">center</property>
</object>
</child>
<child>
......
......@@ -11,7 +11,7 @@
<object class="GtkOverlay">
<property name="margin-bottom">4</property>
<child>
<object class="ArtStack" id="_art_stack">
<object class="GtkImage" id="_cover_image">
<property name="hexpand">True</property>
<child>
<object class="GtkGestureClick">
......
......@@ -7,8 +7,9 @@
<property name="orientation">horizontal</property>
<property name="margin-start">10</property>
<child>
<object class="ArtStack" id="_art_stack">
<property name="focusable">False</property>
<object class="GtkImage" id="_cover_image">
<property name="halign">center</property>
<property name="valign">center</property>
</object>
</child>
<child>
......
......@@ -17,8 +17,7 @@
<property name="margin-top">6</property>
<signal name="query-tooltip" handler="_on_tooltip_query"/>
<child>
<object class="ArtStack" id="_art_stack">
<property name="focusable">False</property>
<object class="GtkImage" id="_cover_image">
</object>
</child>
<child>
......
......@@ -72,9 +72,15 @@ class AsyncQueue(GObject.GObject):
"""
async_obj_id = id(args[0])
if (async_obj_id not in self._async_pool
and async_obj_id not in self._async_active_pool):
self._async_pool[async_obj_id] = (args)
if async_obj_id in self._async_active_pool:
obj = args[0]
handler_id, _ = self._async_data.pop(obj)
obj.disconnect(handler_id)
self._async_active_pool.pop(id(obj))
elif async_obj_id in self._async_pool:
self._async_pool.pop(async_obj_id)
self._async_pool[async_obj_id] = (args)
if self._timeout_id == 0:
self._timeout_id = GLib.timeout_add(100, self._dispatch)
......@@ -97,6 +103,11 @@ class AsyncQueue(GObject.GObject):
tick)
async_obj.start(*async_task_args[1:])
self._log.debug(
f"{self._queue_name}: "
f"{len(self._async_active_pool)} active task(s)"
f"with {len(self._async_pool)} remaining")
return GLib.SOURCE_CONTINUE
def _on_async_finished(
......@@ -105,10 +116,5 @@ class AsyncQueue(GObject.GObject):
t = (time.time() - tick) * 1000
self._log.debug(f"{self._queue_name}: {t:.2f} ms task")
a = len(self._async_active_pool)
self._log.debug(
f"{self._queue_name}: "
f"{a} active task(s) of {len(self._async_pool) + a}")
obj.disconnect(handler_id)
self._async_active_pool.pop(id(obj))
......@@ -80,7 +80,13 @@ class CoreAlbum(GObject.GObject):
disc_model_sort = Gtk.SortListModel.new(disc_model)
def _disc_order_sort(disc_a, disc_b, data=None):
return disc_a.props.disc_nr - disc_b.props.disc_nr
order = disc_a.props.disc_nr - disc_b.props.disc_nr
if order < 0:
return Gtk.Ordering.SMALLER
elif order > 0:
return Gtk.Ordering.LARGER
else:
return Gtk.Ordering.EQUAL
disc_sorter = Gtk.CustomSorter()
disc_sorter.set_sort_func(_disc_order_sort)
......
......@@ -58,7 +58,13 @@ class CoreDisc(GObject.GObject):
@GObject.Property(type=Gio.ListModel, default=None)
def model(self):
def _disc_sort(song_a, song_b, data=None):
return song_a.props.track_number - song_b.props.track_number
order = song_a.props.track_number - song_b.props.track_number
if order < 0:
return Gtk.Ordering.SMALLER
elif order > 0:
return Gtk.Ordering.LARGER
else:
return Gtk.Ordering.EQUAL
if self._model is None:
self._filter_model = Gtk.FilterListModel.new(
......
......@@ -23,12 +23,22 @@
# delete this exception statement from your version.
from __future__ import annotations
from typing import Optional, Union
import typing
import gi
gi.require_versions({"Gdk": "4.0", "Gtk": "4.0", "Gsk": "4.0"})
from gi.repository import Gsk, Gtk, GObject, Graphene, Gdk
from gi.repository import Adw, Gsk, Gtk, GObject, Graphene, Gdk
from gnomemusic.texturecache import TextureCache
from gnomemusic.utils import ArtSize, DefaultIconType
if typing.TYPE_CHECKING:
from gnomemusic.corealbum import CoreAlbum
from gnomemusic.coreartist import CoreArtist
from gnomemusic.coresong import CoreSong
if typing.TYPE_CHECKING:
CoreObject = Union[CoreAlbum, CoreArtist, CoreSong]
class CoverPaintable(GObject.GObject, Gdk.Paintable):
......@@ -41,28 +51,29 @@ class CoverPaintable(GObject.GObject, Gdk.Paintable):
__gtype_name__ = "CoverPaintable"
def __init__(
self, art_size: ArtSize, widget: Gtk.Widget,
icon_type: DefaultIconType = DefaultIconType.ALBUM,
texture: Gdk.Texture = None, dark: bool = False) -> None:
self, widget: Gtk.Widget, art_size: ArtSize,
icon_type: DefaultIconType) -> None:
"""Initiliaze CoverPaintable
:param ArtSize art_size: Size of the cover
:param Gtk.Widget widget: Widget using the cover
:param ArtSize art_size: Size of the cover
:param DefaultIconType icon_type: Type of cover
:param Gdk.Texture texture: Texture to use or None for
placeholder
:param bool dark: Dark mode
"""
super().__init__()
self._art_size = art_size
self._dark = dark
self._coreobject: Optional[CoreObject] = None
self._icon_theme = Gtk.IconTheme.new().get_for_display(
widget.get_display())
self._icon_type = icon_type
self._texture = texture
self._style_manager = Adw.StyleManager.get_default()
self._texture = None
self._texture_cache = TextureCache()
self._thumbnail_id = 0
self._widget = widget
self._style_manager.connect("notify::dark", self._on_dark_changed)
def do_snapshot(self, snapshot: Gtk.Snapshot, w: int, h: int) -> None:
if self._icon_type == DefaultIconType.ARTIST:
radius = 90.0
......@@ -94,9 +105,10 @@ class CoverPaintable(GObject.GObject, Gdk.Paintable):
self._icon_type.value, None, w * i_s,
self._widget.props.scale_factor, 0, 0)
bg_color = Gdk.RGBA(1, 1, 1, 1)
if self._dark:
bg_color = Gdk.RGBA(0.3, 0.3, 0.3, 1)
bg_color = Gdk.RGBA()
bg_color.parse("rgba(95%, 95%, 95%, 1)")
if self._style_manager.props.dark:
bg_color.parse("rgba(30%, 30%, 30%, 1)")
snapshot.append_color(bg_color, Graphene.Rect().init(0, 0, w, h))
snapshot.translate(
......@@ -108,8 +120,93 @@ class CoverPaintable(GObject.GObject, Gdk.Paintable):
snapshot.pop()
def _on_dark_changed(
self, style_manager: Adw.StyleManager,
pspec: GObject.ParamSpecBoolean) -> None:
if self._texture is not None:
return
self.invalidate_contents()
@GObject.Property(type=object, default=None)
def coreobject(self) -> Optional[CoreObject]:
"""Get the current core object in use
:returns: The corrent coreobject
:rtype: Union[CoreAlbum, CoreArtist, CoreSong] or None
"""
return self._coreobject
@coreobject.setter # type: ignore
def coreobject(self, coreobject: CoreObject) -> None:
"""Update the coreobject used for CoverPaintable
:param Union[CoreAlbum, CoreArtist, CoreSong] coreobject:
The coreobject to set
"""
if coreobject is self._coreobject:
return
self._texture_cache.clear_pending_lookup_callback()
if self._texture:
self._texture = None
self.invalidate_contents()
if self._thumbnail_id != 0:
self._coreobject.disconnect(self._thumbnail_id)
self._thumbnail_id = 0
self._coreobject = coreobject
self._thumbnail_id = self._coreobject.connect(
"notify::thumbnail", self._on_thumbnail_changed)
if self._coreobject.props.thumbnail is not None:
self._on_thumbnail_changed(self._coreobject, None)
def _on_thumbnail_changed(
self, coreobject: CoreObject,
uri: GObject.ParamSpecString) -> None:
thumbnail_uri = coreobject.props.thumbnail
if thumbnail_uri == "generic":
self._texture = None
self.invalidate_contents()
return
self._texture_cache.connect("texture", self._on_texture_cache)
self._texture_cache.lookup(thumbnail_uri)
def _on_texture_cache(
self, texture_cache: TextureCache, texture: Gdk.Texture) -> None:
if texture == self._texture:
return
self._texture = texture
self.invalidate_contents()
@GObject.Property(type=object, flags=GObject.ParamFlags.READWRITE)
def icon_type(self) -> DefaultIconType:
"""Icon type of the cover
:returns: The type of the default icon
:rtype: DefaultIconType
"""
return self._icon_type
@icon_type.setter # type: ignore
def icon_type(self, value: DefaultIconType) -> None:
"""Set the cover icon type
:param DefaultIconType value: The default icon type for the
cover
"""
self._icon_type = value
self.invalidate_contents()
def do_get_flags(self) -> Gdk.PaintableFlags:
return Gdk.PaintableFlags.SIZE | Gdk.PaintableFlags.CONTENTS
return Gdk.PaintableFlags.SIZE
def do_get_intrinsic_height(self) -> int:
return self._art_size.height
......
# Copyright 2021 The GNOME Music developers
#
# GNOME Music is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# GNOME Music is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# The GNOME Music authors hereby grant permission for non-GPL compatible
# GStreamer plugins to be used and distributed together with GStreamer
# and GNOME Music. This permission is above and beyond the permissions
# granted by the GPL license by which GNOME Music is covered. If you
# modify this code, you may extend this exception to your version of the
# code, but you are not obligated to do so. If you do not wish to do so,
# delete this exception statement from your version.
from __future__ import annotations
from typing import Dict, Tuple
from gi.repository import Adw, Gtk, GObject
from gnomemusic.coverpaintable import CoverPaintable
from gnomemusic.utils import ArtSize, DefaultIconType
class DefaultIcon(GObject.GObject):
"""Provides the symbolic fallback icons."""
_cache: Dict[
Tuple[DefaultIconType, ArtSize, int, bool], CoverPaintable] = {}
def __init__(self, widget: Gtk.Widget) -> None:
"""Initialize DefaultIcon
:param Gtk.Widget widget: The widget of the icon
"""
super().__init__()
self._widget = widget
def _make_default_icon(
self, icon_type: DefaultIconType, art_size: ArtSize,
dark: bool) -> CoverPaintable:
paintable = CoverPaintable(
art_size, self._widget, icon_type=icon_type, dark=dark)
return paintable
def get(self, icon_type: DefaultIconType,
art_size: ArtSize) -> CoverPaintable:
"""Returns the requested symbolic icon
Returns a paintable of the requested symbolic icon in the
given size, shape and color.
:param DefaultIconType icon_type: The type of icon
:param ArtSize art_size: The size requested
:return: The symbolic icon
:rtype: CoverPaintable
"""
dark = Adw.StyleManager.get_default().props.dark
scale = self._widget.props.scale_factor
if (icon_type, art_size, scale, dark) not in self._cache.keys():
new_icon = self._make_default_icon(icon_type, art_size, dark)
self._cache[(icon_type, art_size, scale, dark)] = new_icon
return self._cache[(icon_type, art_size, scale, dark)]
......@@ -177,6 +177,7 @@ class EmbeddedArt(GObject.GObject):
except GLib.Error as error:
# File already exists.
self._log.info(f"Error: {error.domain}, {error.message}")
self.emit("art-found", True)
else:
pixbuf.save_to_streamv_async(
output_stream, "jpeg", None, None, None,
......
# Copyright 2020 The GNOME Music developers
# Copyright 2022 The GNOME Music developers
#
# GNOME Music is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -22,23 +22,21 @@
# code, but you are not obligated to do so. If you do not wish to do so,
# delete this exception statement from your version.
from gi.repository import Gdk, GdkPixbuf, Gio, Gtk, GLib, GObject
from __future__ import annotations
from typing import Any
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, GObject
from gnomemusic.coreartist import CoreArtist
from gnomemusic.coverpaintable import CoverPaintable
from gnomemusic.defaulticon import DefaultIcon
from gnomemusic.musiclogger import MusicLogger
from gnomemusic.utils import ArtSize, DefaultIconType
class ArtCache(GObject.GObject):
class MediaArtLoader(GObject.GObject):
"""Handles retrieval of MediaArt cache art
Uses signals to indicate success or failure and always returns a
CoverPaintable.
Signals when the media is loaded and passes a texture or None.
"""
__gtype_name__ = "ArtCache"
__gtype_name__ = "MediaArtLoader"
__gsignals__ = {
"finished": (GObject.SignalFlags.RUN_FIRST, None, (object, ))
......@@ -46,83 +44,61 @@ class ArtCache(GObject.GObject):
_log = MusicLogger()
def __init__(self, widget: Gtk.Widget) -> None:
"""Intialize ArtCache
:param Gtk.Widget widget: The widget of this cache
def __init__(self) -> None:
"""Intialize MediaArtLoader
"""
super().__init__()
self._size = ArtSize.SMALL
self._widget = widget
self._coreobject = None
self._icon_type = DefaultIconType.ALBUM
self._paintable = None
self._texture: Gdk.Texture
def start(self, coreobject, size):
def start(self, uri: str) -> None:
"""Start the cache query
:param coreobject: The object to search art for
:param ArtSize size: The desired size
:param str uri: The MediaArt uri
"""
self._coreobject = coreobject
self._size = size
if isinstance(coreobject, CoreArtist):
self._icon_type = DefaultIconType.ARTIST
self._paintable = DefaultIcon(self._widget).get(
self._icon_type, self._size)
thumb_file = Gio.File.new_for_uri(uri)
thumbnail_uri = coreobject.props.thumbnail
if thumbnail_uri == "generic":
self.emit("finished", self._paintable)
return
thumb_file = Gio.File.new_for_uri(thumbnail_uri)
if thumb_file:
thumb_file.read_async(
GLib.PRIORITY_DEFAULT_IDLE, None, self._open_stream, None)
return
self.emit("finished", self._paintable)
else:
self.emit("finished", None)
def _open_stream(self, thumb_file, result, arguments):
def _open_stream(
self, thumb_file: Gio.File, result: Gio.AsyncResult,
arguments: Any) -> None:
try:
stream = thumb_file.read_finish(result)
except GLib.Error as error:
self._log.warning(
"Error: {}, {}".format(error.domain, error.message))
self.emit("finished", self._paintable)
self.emit("finished", None)
return
GdkPixbuf.Pixbuf.new_from_stream_async(
stream, None, self._pixbuf_loaded, None)
stream, None, self._pixbuf_loaded)
def _pixbuf_loaded(self, stream, result, data):
def _pixbuf_loaded(
self, stream: Gio.InputStream, result: Gio.AsyncResult) -> None:
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_stream_finish(result)
except GLib.Error as error:
self._log.warning(
"Error: {}, {}".format(error.domain, error.message))
self.emit("finished", self._paintable)
self.emit("finished", None)
return
texture = Gdk.Texture.new_for_pixbuf(pixbuf)
if texture:
self._paintable = CoverPaintable(
self._size, self._widget, icon_type=self._icon_type,
texture=texture)
self._texture = Gdk.Texture.new_for_pixbuf(pixbuf)
stream.close_async(
GLib.PRIORITY_DEFAULT_IDLE, None, self._close_stream, None)
GLib.PRIORITY_DEFAULT_IDLE, None, self._close_stream)
def _close_stream(self, stream, result, data):
def _close_stream(
self, stream: Gio.InputStream, result: Gio.AsyncResult) -> None:
try:
stream.close_finish(result)
except GLib.Error as error:
self._log.warning(
"Error: {}, {}".format(error.domain, error.message))
self.emit("finished", self._paintable)
self.emit("finished", self._texture)
......@@ -49,6 +49,7 @@ class MusicLogger(GObject.GObject):
function = stack[2][3]
if level in [GLib.LogLevelFlags.LEVEL_DEBUG,
GLib.LogLevelFlags.LEVEL_INFO,
GLib.LogLevelFlags.LEVEL_WARNING]:
message = "({}, {}, {}) {}".format(
filename, function, line, message)
......
......@@ -277,7 +277,12 @@ class PlayerPlaylist(GObject.GObject):
def _on_repeat_mode_changed(self, klass, param):
def _shuffle_sort(song_a, song_b, data=None):
return song_a.shuffle_pos < song_b.shuffle_pos
if song_a.props.shuffle_pos < song_b.props.shuffle_pos:
return Gtk.Ordering.SMALLER
elif song_a.props.shuffle_pos > song_b.props.shuffle_pos:
return Gtk.Ordering.LARGER
else:
return Gtk.Ordering.EQUAL
if self.props.repeat_mode == RepeatMode.SHUFFLE:
for idx, coresong in enumerate(self._model):
......
......@@ -138,6 +138,7 @@ class StoreArt(GObject.Object):
except GLib.Error as error:
# File already exists.
self._log.info(f"Error: {error.domain}, {error.message}")
self.emit("finished")
else:
pixbuf.save_to_streamv_async(
output_stream, "jpeg", None, None, None,
......
# Copyright 2022 The GNOME Music developers
#
# GNOME Music is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# GNOME Music is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with GNOME Music; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# The GNOME Music authors hereby grant permission for non-GPL compatible
# GStreamer plugins to be used and distributed together with GStreamer
# and GNOME Music. This permission is above and beyond the permissions
# granted by the GPL license by which GNOME Music is covered. If you
# modify this code, you may extend this exception to your version of the
# code, but you are not obligated to do so. If you do not wish to do so,
# delete this exception statement from your version.
from __future__ import annotations
from enum import IntEnum
from typing import Dict, Optional, Tuple, Union
import time
import typing
from gi.repository import GLib, GObject, Gdk, Gio
from gnomemusic.asyncqueue import AsyncQueue
from gnomemusic.musiclogger import MusicLogger
from gnomemusic.mediaartloader import MediaArtLoader
if typing.TYPE_CHECKING:
from gnomemusic.corealbum import CoreAlbum
from gnomemusic.coreartist import CoreArtist
from gnomemusic.coresong import CoreSong
if typing.TYPE_CHECKING:
CoreObject = Union[CoreAlbum, CoreArtist, CoreSong]
class TextureCache(GObject.GObject):
"""Retrieval and cache for artwork textures
"""
class LoadingState(IntEnum):
"""The loading status of the URI
AVAILABLE: The texture is currently cached
UNAVAILABLE: No texture is available for the URI
CLEARED: The texture was available, has been cleared and
should be available on lookup
"""
AVAILABLE = 0
UNAVAILABLE = 1
CLEARED = 2
__gtype_name__ = "TextureCache"
__gsignals__ = {
"texture": (GObject.SignalFlags.RUN_FIRST, None, (object, ))
}
# Music has two main cycling views (AlbumsView and ArtistsView),
# both have around 200 cycling items each when fully used. For
# the cache to be useful it needs to be larger than the given
# numbers combined.
_MAX_CACHE_SIZE = 800
_async_queue = AsyncQueue("TextureCache")
_cleanup_id = 0
_log = MusicLogger()
_memory_monitor = Gio.MemoryMonitor.dup_default()
_size = _MAX_CACHE_SIZE
_textures: Dict[str, Tuple[
TextureCache.LoadingState, float, Optional[Gdk.Texture]]] = {}
def __init__(self) -> None:
"""Initialize Texturecache
"""
super().__init__()
self._art_loader: MediaArtLoader
self._art_loading_id = 0
if TextureCache._cleanup_id == 0:
TextureCache._cleanup_id = GLib.timeout_add_seconds(
10, TextureCache._cache_cleanup)
TextureCache._memory_monitor.connect(
"low-memory-warning", TextureCache._low_memory_warning)
def clear_pending_lookup_callback(self) -> None:
"""Disconnect ongoing lookup callback
"""
if self._art_loading_id != 0:
self._art_loader.disconnect(self._art_loading_id)
self._art_loading_id = 0
def lookup(self, uri: str) -> None:
"""Look up a texture for the given MediaArt uri
:param str uri: The MediaArt uri
"""
self.clear_pending_lookup_callback()
if uri in TextureCache._textures.keys():
state, _, texture = TextureCache._textures[uri]
if state in [
TextureCache.LoadingState.AVAILABLE,
TextureCache.LoadingState.UNAVAILABLE]:
self.emit("texture", texture)
TextureCache._textures[uri] = (state, time.time(), texture)
return
self._art_loader = MediaArtLoader()
self._art_loading_id = self._art_loader.connect(
"finished", self._on_art_loading_finished, uri)
self._async_queue.queue(self._art_loader, uri)
@classmethod
def _low_memory_warning(
cls, mm: Gio.MemoryMonitor,
level: Gio.MemoryMonitorWarningLevel) -> None:
if level < Gio.MemoryMonitorWarningLevel.LOW:
TextureCache._size = TextureCache._MAX_CACHE_SIZE
else:
# List slicing with 0 gives an empty list in
# _cache_cleanup.
TextureCache._size = 1
@classmethod
def _cache_cleanup(cls) -> None:
"""Sorts the available cache entries by recency and evicts
the oldest items to match the maximum cache size.
"""
sorted_available = {
k: (state, t, texture)
for k, (state, t, texture) in sorted(
TextureCache._textures.items(), key=lambda item: item[1][1])
if state in [TextureCache.LoadingState.AVAILABLE]}
sorted_available_l = len(sorted_available)
if sorted_available_l < TextureCache._size:
return GLib.SOURCE_CONTINUE
keys_to_clear = list(sorted_available.keys())[:-TextureCache._size]
for key in keys_to_clear:
state, t, texture = TextureCache._textures[key]
TextureCache._textures[key] = (
TextureCache.LoadingState.CLEARED, t, None)
keys_l = len(keys_to_clear)
TextureCache._log.info(
f"Cleared {keys_l} items, texture cache contains"
f" {sorted_available_l-keys_l} available items.")
return GLib.SOURCE_CONTINUE
def _on_art_loading_finished(
self, art_loader: MediaArtLoader, texture: Gdk.Texture,
uri: str) -> None:
if texture:
state = TextureCache.LoadingState.AVAILABLE
else:
state = TextureCache.LoadingState.UNAVAILABLE
TextureCache._textures[uri] = (state, time.time(), texture)
self.emit("texture", texture)
......@@ -172,7 +172,7 @@ def seconds_to_string(duration):
minutes = seconds // 60
seconds %= 60
return '{:d}{:02d}'.format(minutes, seconds)
return '{:d}:{:02d}'.format(minutes, seconds)
def normalize_caseless(text):
......