Merge branch 'mpris-dbus-support'
This commit is contained in:
@@ -262,17 +262,19 @@ def generate_class_for_type(type_name):
|
||||
|
||||
|
||||
with open(output_file, 'w+') as outfile:
|
||||
outfile.writelines('\n'.join([
|
||||
'"""',
|
||||
'WARNING: AUTOGENERATED FILE',
|
||||
'This file was generated by the api_object_generator.py script. Do',
|
||||
'not modify this file directly, rather modify the script or run it on',
|
||||
'a new API version.',
|
||||
'"""',
|
||||
'',
|
||||
'from datetime import datetime',
|
||||
'from typing import List',
|
||||
'from enum import Enum',
|
||||
'from libremsonic.server.api_object import APIObject',
|
||||
*map(generate_class_for_type, output_order),
|
||||
]) + '\n')
|
||||
outfile.writelines(
|
||||
'\n'.join(
|
||||
[
|
||||
'"""',
|
||||
'WARNING: AUTOGENERATED FILE',
|
||||
'This file was generated by the api_object_generator.py script. Do',
|
||||
'not modify this file directly, rather modify the script or run it on',
|
||||
'a new API version.',
|
||||
'"""',
|
||||
'',
|
||||
'from datetime import datetime',
|
||||
'from typing import List',
|
||||
'from enum import Enum',
|
||||
'from libremsonic.server.api_object import APIObject',
|
||||
*map(generate_class_for_type, output_order),
|
||||
]) + '\n')
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import os
|
||||
import re
|
||||
import math
|
||||
import random
|
||||
|
||||
from os import environ
|
||||
import concurrent.futures
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
@@ -13,6 +15,7 @@ from .ui.main import MainWindow
|
||||
from .ui.configure_servers import ConfigureServersDialog
|
||||
from .ui.settings import SettingsDialog
|
||||
|
||||
from .dbus_manager import DBusManager, dbus_propagate
|
||||
from .state_manager import ApplicationState, RepeatType
|
||||
from .cache_manager import CacheManager
|
||||
from .server.api_objects import Child
|
||||
@@ -55,9 +58,9 @@ class LibremsonicApp(Gtk.Application):
|
||||
config_file = config_file.get_bytestring().decode('utf-8')
|
||||
else:
|
||||
# Default to ~/.config/libremsonic.
|
||||
config_folder = (environ.get('XDG_CONFIG_HOME')
|
||||
or environ.get('APPDATA')
|
||||
or os.path.join(environ.get('HOME'), '.config'))
|
||||
config_folder = (
|
||||
environ.get('XDG_CONFIG_HOME') or environ.get('APPDATA')
|
||||
or os.path.join(environ.get('HOME'), '.config'))
|
||||
config_folder = os.path.join(config_folder, 'libremsonic')
|
||||
config_file = os.path.join(config_folder, 'config.json')
|
||||
|
||||
@@ -89,18 +92,16 @@ class LibremsonicApp(Gtk.Application):
|
||||
add_action('prev-track', self.on_prev_track)
|
||||
add_action('repeat-press', self.on_repeat_press)
|
||||
add_action('shuffle-press', self.on_shuffle_press)
|
||||
add_action('play-queue-click',
|
||||
self.on_play_queue_click,
|
||||
parameter_type='s')
|
||||
add_action(
|
||||
'play-queue-click', self.on_play_queue_click, parameter_type='s')
|
||||
|
||||
# Navigation actions.
|
||||
add_action('play-next', self.on_play_next, parameter_type='as')
|
||||
add_action('add-to-queue', self.on_add_to_queue, parameter_type='as')
|
||||
add_action('go-to-album', self.on_go_to_album, parameter_type='s')
|
||||
add_action('go-to-artist', self.on_go_to_artist, parameter_type='s')
|
||||
add_action('go-to-playlist',
|
||||
self.on_go_to_playlist,
|
||||
parameter_type='s')
|
||||
add_action(
|
||||
'go-to-playlist', self.on_go_to_playlist, parameter_type='s')
|
||||
|
||||
add_action('mute-toggle', self.on_mute_toggle)
|
||||
add_action(
|
||||
@@ -109,10 +110,9 @@ class LibremsonicApp(Gtk.Application):
|
||||
)
|
||||
|
||||
def do_activate(self):
|
||||
|
||||
# We only allow a single window and raise any existing ones
|
||||
if self.window:
|
||||
self.show_window()
|
||||
self.window.present()
|
||||
return
|
||||
|
||||
# Windows are associated with the application when the last one is
|
||||
@@ -126,8 +126,8 @@ class LibremsonicApp(Gtk.Application):
|
||||
os.path.join(os.path.dirname(__file__), 'ui/app_styles.css'))
|
||||
context = Gtk.StyleContext()
|
||||
screen = Gdk.Screen.get_default()
|
||||
context.add_provider_for_screen(screen, css_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
context.add_provider_for_screen(
|
||||
screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
self.window.stack.connect(
|
||||
'notify::visible-child',
|
||||
@@ -136,13 +136,14 @@ class LibremsonicApp(Gtk.Application):
|
||||
self.window.connect('song-clicked', self.on_song_clicked)
|
||||
self.window.connect('refresh-window', self.on_refresh_window)
|
||||
self.window.player_controls.connect('song-scrub', self.on_song_scrub)
|
||||
self.window.player_controls.connect('device-update',
|
||||
self.on_device_update)
|
||||
self.window.player_controls.connect('volume-change',
|
||||
self.on_volume_change)
|
||||
self.window.player_controls.connect(
|
||||
'device-update', self.on_device_update)
|
||||
self.window.player_controls.connect(
|
||||
'volume-change', self.on_volume_change)
|
||||
self.window.connect('key-press-event', self.on_window_key_press)
|
||||
|
||||
self.show_window()
|
||||
self.window.show_all()
|
||||
self.window.present()
|
||||
|
||||
# Load the configuration and update the UI with the curent server, if
|
||||
# it exists.
|
||||
@@ -185,6 +186,7 @@ class LibremsonicApp(Gtk.Application):
|
||||
|
||||
GLib.idle_add(self.on_next_track)
|
||||
|
||||
@dbus_propagate(self)
|
||||
def on_player_event(event: PlayerEvent):
|
||||
if event.name == 'play_state_change':
|
||||
self.state.playing = event.value
|
||||
@@ -208,6 +210,8 @@ class LibremsonicApp(Gtk.Application):
|
||||
)
|
||||
self.player = self.mpv_player
|
||||
|
||||
self.player.volume = self.state.volume
|
||||
|
||||
if self.state.current_device != 'this device':
|
||||
# TODO figure out how to activate the chromecast if possible
|
||||
# without blocking the main thread. Also, need to make it obvious
|
||||
@@ -221,7 +225,175 @@ class LibremsonicApp(Gtk.Application):
|
||||
if self.current_server.sync_enabled:
|
||||
self.update_play_state_from_server(prompt_confirm=True)
|
||||
|
||||
# Send out to the bus that we exist.
|
||||
self.dbus_manager.property_diff()
|
||||
|
||||
# ########## DBUS MANAGMENT ########## #
|
||||
def do_dbus_register(self, connection, path):
|
||||
self.dbus_manager = DBusManager(
|
||||
connection,
|
||||
self.on_dbus_method_call,
|
||||
self.on_dbus_set_property,
|
||||
lambda: (self.state, self.player),
|
||||
)
|
||||
return True
|
||||
|
||||
def on_dbus_method_call(
|
||||
self,
|
||||
connection,
|
||||
sender,
|
||||
path,
|
||||
interface,
|
||||
method,
|
||||
params,
|
||||
invocation,
|
||||
):
|
||||
second_microsecond_conversion = 1000000
|
||||
track_id_re = re.compile(r'/song/(.*)')
|
||||
playlist_id_re = re.compile(r'/playlist/(.*)')
|
||||
|
||||
def seek_fn(offset):
|
||||
offset_seconds = offset / second_microsecond_conversion
|
||||
new_seconds = self.state.song_progress + offset_seconds
|
||||
self.on_song_scrub(
|
||||
None, new_seconds / self.state.current_song.duration * 100)
|
||||
|
||||
def set_pos_fn(track_id, position=0):
|
||||
if self.state.playing:
|
||||
self.on_play_pause()
|
||||
pos_seconds = position / second_microsecond_conversion
|
||||
self.state.song_progress = pos_seconds
|
||||
self.play_song(track_id_re.match(track_id).group(1))
|
||||
|
||||
def get_track_metadata(track_ids):
|
||||
metadatas = []
|
||||
|
||||
song_details_futures = [
|
||||
CacheManager.get_song_details(track_id) for track_id in (
|
||||
track_id_re.match(tid).group(1) for tid in track_ids)
|
||||
]
|
||||
for f in concurrent.futures.wait(song_details_futures).done:
|
||||
metadata = self.dbus_manager.get_mpris_metadata(f.result())
|
||||
metadatas.append(
|
||||
{
|
||||
k: DBusManager.to_variant(v)
|
||||
for k, v in metadata.items()
|
||||
})
|
||||
|
||||
return GLib.Variant('(aa{sv})', (metadatas, ))
|
||||
|
||||
def activate_playlist(playlist_id):
|
||||
playlist_id = playlist_id_re.match(playlist_id).group(1)
|
||||
playlist = CacheManager.get_playlist(playlist_id).result()
|
||||
|
||||
# Calculate the song id to play.
|
||||
song_id = playlist.entry[0].id
|
||||
if self.state.shuffle_on:
|
||||
rand_idx = random.randint(0, len(playlist.entry) - 1)
|
||||
song_id = playlist.entry[rand_idx].id
|
||||
|
||||
self.on_song_clicked(
|
||||
None,
|
||||
song_id,
|
||||
[s.id for s in playlist.entry],
|
||||
{'active_playlist_id': playlist_id},
|
||||
)
|
||||
|
||||
def get_playlists(index, max_count, order, reverse_order):
|
||||
playlists = CacheManager.get_playlists().result()
|
||||
sorters = {
|
||||
'Alphabetical': lambda p: p.name,
|
||||
'Created': lambda p: p.created,
|
||||
'Modified': lambda p: p.changed,
|
||||
}
|
||||
playlists.sort(
|
||||
key=sorters.get(order, lambda p: p),
|
||||
reverse=reverse_order,
|
||||
)
|
||||
|
||||
return GLib.Variant(
|
||||
'(a(oss))', (
|
||||
[
|
||||
(
|
||||
'/playlist/' + p.id,
|
||||
p.name,
|
||||
CacheManager.get_cover_art_filename(
|
||||
p.coverArt,
|
||||
allow_download=False,
|
||||
).result() or '',
|
||||
)
|
||||
for p in playlists[index:(index + max_count)]
|
||||
if p.songCount > 0
|
||||
], ))
|
||||
|
||||
method_call_map = {
|
||||
'org.mpris.MediaPlayer2': {
|
||||
'Raise': self.window.present,
|
||||
'Quit': self.window.destroy,
|
||||
},
|
||||
'org.mpris.MediaPlayer2.Player': {
|
||||
'Next': self.on_next_track,
|
||||
'Previous': self.on_prev_track,
|
||||
'Pause': self.state.playing and self.on_play_pause,
|
||||
'PlayPause': self.on_play_pause,
|
||||
'Stop': self.state.playing and self.on_play_pause,
|
||||
'Play': not self.state.playing and self.on_play_pause,
|
||||
'Seek': seek_fn,
|
||||
'SetPosition': set_pos_fn,
|
||||
},
|
||||
'org.mpris.MediaPlayer2.TrackList': {
|
||||
'GoTo': set_pos_fn,
|
||||
'GetTracksMetadata': get_track_metadata,
|
||||
},
|
||||
'org.mpris.MediaPlayer2.Playlists': {
|
||||
'ActivatePlaylist': activate_playlist,
|
||||
'GetPlaylists': get_playlists,
|
||||
},
|
||||
}
|
||||
method = method_call_map.get(interface, {}).get(method)
|
||||
if method is None:
|
||||
print(f'Unknown/unimplemented method: {interface}.{method}')
|
||||
invocation.return_value(method(*params) if callable(method) else None)
|
||||
|
||||
def on_dbus_set_property(
|
||||
self,
|
||||
connection,
|
||||
sender,
|
||||
path,
|
||||
interface,
|
||||
property_name,
|
||||
value,
|
||||
):
|
||||
def change_loop(new_loop_status):
|
||||
self.state.repeat_type = RepeatType.from_mpris_loop_status(
|
||||
new_loop_status.get_string())
|
||||
self.update_window()
|
||||
|
||||
def set_shuffle(new_val):
|
||||
if new_val.get_boolean() != self.state.shuffle_on:
|
||||
self.on_shuffle_press(None, None)
|
||||
|
||||
def set_volume(new_val):
|
||||
self.on_volume_change(None, value.get_double() * 100)
|
||||
|
||||
setter_map = {
|
||||
'org.mpris.MediaPlayer2.Player': {
|
||||
'LoopStatus': change_loop,
|
||||
'Rate': lambda _: None,
|
||||
'Shuffle': set_shuffle,
|
||||
'Volume': set_volume,
|
||||
}
|
||||
}
|
||||
|
||||
setter = setter_map.get(interface).get(property_name)
|
||||
if setter is None:
|
||||
print('Set: Unknown property:', setter)
|
||||
return
|
||||
if callable(setter):
|
||||
setter(value)
|
||||
|
||||
# ########## ACTION HANDLERS ########## #
|
||||
@dbus_propagate()
|
||||
def on_refresh_window(self, _, state_updates, force=False):
|
||||
for k, v in state_updates.items():
|
||||
setattr(self.state, k, v)
|
||||
@@ -251,6 +423,7 @@ class LibremsonicApp(Gtk.Application):
|
||||
self.reset_cache_manager()
|
||||
dialog.destroy()
|
||||
|
||||
@dbus_propagate()
|
||||
def on_play_pause(self, *args):
|
||||
if self.state.current_song is None:
|
||||
return
|
||||
@@ -273,6 +446,9 @@ class LibremsonicApp(Gtk.Application):
|
||||
current_idx = current_idx - 1
|
||||
# Wrap around the play queue if at the end.
|
||||
elif current_idx == len(self.state.play_queue) - 1:
|
||||
# This may happen due to D-Bus.
|
||||
if self.state.repeat_type == RepeatType.NO_REPEAT:
|
||||
return
|
||||
current_idx = -1
|
||||
|
||||
self.play_song(self.state.play_queue[current_idx + 1], reset=True)
|
||||
@@ -295,12 +471,14 @@ class LibremsonicApp(Gtk.Application):
|
||||
|
||||
self.play_song(self.state.play_queue[song_to_play], reset=True)
|
||||
|
||||
@dbus_propagate()
|
||||
def on_repeat_press(self, action, params):
|
||||
# Cycle through the repeat types.
|
||||
new_repeat_type = RepeatType((self.state.repeat_type.value + 1) % 3)
|
||||
self.state.repeat_type = new_repeat_type
|
||||
self.update_window()
|
||||
|
||||
@dbus_propagate()
|
||||
def on_shuffle_press(self, action, params):
|
||||
if self.state.shuffle_on:
|
||||
# Revert to the old play queue.
|
||||
@@ -320,6 +498,7 @@ class LibremsonicApp(Gtk.Application):
|
||||
def on_play_queue_click(self, action, song_id):
|
||||
self.play_song(song_id.get_string(), reset=True)
|
||||
|
||||
@dbus_propagate()
|
||||
def on_play_next(self, action, song_ids):
|
||||
if self.state.current_song is None:
|
||||
insert_at = 0
|
||||
@@ -327,12 +506,13 @@ class LibremsonicApp(Gtk.Application):
|
||||
insert_at = (
|
||||
self.state.play_queue.index(self.state.current_song.id) + 1)
|
||||
|
||||
self.state.play_queue = (self.state.play_queue[:insert_at]
|
||||
+ list(song_ids)
|
||||
+ self.state.play_queue[insert_at:])
|
||||
self.state.play_queue = (
|
||||
self.state.play_queue[:insert_at] + list(song_ids)
|
||||
+ self.state.play_queue[insert_at:])
|
||||
self.state.old_play_queue.extend(song_ids)
|
||||
self.update_window()
|
||||
|
||||
@dbus_propagate()
|
||||
def on_add_to_queue(self, action, song_ids):
|
||||
self.state.play_queue.extend(song_ids)
|
||||
self.state.old_play_queue.extend(song_ids)
|
||||
@@ -386,6 +566,11 @@ class LibremsonicApp(Gtk.Application):
|
||||
if metadata.get('force_shuffle_state') is not None:
|
||||
self.state.shuffle_on = metadata['force_shuffle_state']
|
||||
|
||||
if metadata.get('active_playlist_id') is not None:
|
||||
self.state.active_playlist_id = metadata.get('active_playlist_id')
|
||||
else:
|
||||
self.state.active_playlist_id = None
|
||||
|
||||
# If shuffle is enabled, then shuffle the playlist.
|
||||
if self.state.shuffle_on:
|
||||
song_queue.remove(song_id)
|
||||
@@ -399,6 +584,7 @@ class LibremsonicApp(Gtk.Application):
|
||||
play_queue=song_queue,
|
||||
)
|
||||
|
||||
@dbus_propagate()
|
||||
def on_song_scrub(self, _, scrub_value):
|
||||
if not hasattr(self.state, 'current_song'):
|
||||
return
|
||||
@@ -424,6 +610,8 @@ class LibremsonicApp(Gtk.Application):
|
||||
self.player.pause()
|
||||
self.player._song_loaded = False
|
||||
self.state.playing = False
|
||||
|
||||
self.dbus_manager.property_diff()
|
||||
self.update_window()
|
||||
|
||||
if device_uuid == 'this device':
|
||||
@@ -434,12 +622,15 @@ class LibremsonicApp(Gtk.Application):
|
||||
|
||||
if was_playing:
|
||||
self.on_play_pause()
|
||||
self.dbus_manager.property_diff()
|
||||
|
||||
@dbus_propagate()
|
||||
def on_mute_toggle(self, action, _):
|
||||
self.state.is_muted = not self.state.is_muted
|
||||
self.player.is_muted = self.state.is_muted
|
||||
self.update_window()
|
||||
|
||||
@dbus_propagate()
|
||||
def on_volume_change(self, _, value):
|
||||
self.state.volume = value
|
||||
self.player.volume = self.state.volume
|
||||
@@ -465,6 +656,7 @@ class LibremsonicApp(Gtk.Application):
|
||||
|
||||
self.state.save()
|
||||
self.save_play_queue()
|
||||
self.dbus_manager.shutdown()
|
||||
CacheManager.shutdown()
|
||||
|
||||
# ########## PROPERTIES ########## #
|
||||
@@ -473,16 +665,12 @@ class LibremsonicApp(Gtk.Application):
|
||||
return self.state.config.servers[self.state.config.current_server]
|
||||
|
||||
# ########## HELPER METHODS ########## #
|
||||
def show_window(self):
|
||||
self.window.show_all()
|
||||
self.window.present()
|
||||
|
||||
def show_configure_servers_dialog(self):
|
||||
"""Show the Connect to Server dialog."""
|
||||
dialog = ConfigureServersDialog(self.window, self.state.config)
|
||||
dialog.connect('server-list-changed', self.on_server_list_changed)
|
||||
dialog.connect('connected-server-changed',
|
||||
self.on_connected_server_changed)
|
||||
dialog.connect(
|
||||
'connected-server-changed', self.on_connected_server_changed)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
@@ -546,13 +734,14 @@ class LibremsonicApp(Gtk.Application):
|
||||
|
||||
def play_song(
|
||||
self,
|
||||
song: Child,
|
||||
song_id: str,
|
||||
reset=False,
|
||||
old_play_queue=None,
|
||||
play_queue=None,
|
||||
):
|
||||
# Do this the old fashioned way so that we can have access to ``reset``
|
||||
# in the callback.
|
||||
@dbus_propagate(self)
|
||||
def do_play_song(song: Child):
|
||||
uri, stream = CacheManager.get_song_filename_or_stream(
|
||||
song,
|
||||
@@ -569,7 +758,7 @@ class LibremsonicApp(Gtk.Application):
|
||||
# TODO someone needs to test this, Dunst doesn't seem to
|
||||
# support it.
|
||||
def on_notification_click(*args):
|
||||
self.show_window()
|
||||
self.window.present()
|
||||
|
||||
try:
|
||||
song_notification = Notify.Notification.new(
|
||||
@@ -615,8 +804,8 @@ class LibremsonicApp(Gtk.Application):
|
||||
if self.player.can_hotswap_source:
|
||||
downloaded_filename = (
|
||||
CacheManager.get_song_filename_or_stream(song)[0])
|
||||
self.player.play_media(downloaded_filename,
|
||||
self.state.song_progress, song)
|
||||
self.player.play_media(
|
||||
downloaded_filename, self.state.song_progress, song)
|
||||
GLib.idle_add(self.update_window)
|
||||
|
||||
# If streaming, also download the song, unless configured not to,
|
||||
@@ -658,7 +847,7 @@ class LibremsonicApp(Gtk.Application):
|
||||
if self.current_server.sync_enabled:
|
||||
CacheManager.scrobble(song.id)
|
||||
|
||||
song_details_future = CacheManager.get_song_details(song)
|
||||
song_details_future = CacheManager.get_song_details(song_id)
|
||||
song_details_future.add_done_callback(
|
||||
lambda f: GLib.idle_add(do_play_song, f.result()), )
|
||||
|
||||
|
@@ -183,9 +183,8 @@ class CacheManager(metaclass=Singleton):
|
||||
cache_meta_file = self.calculate_abs_path('.cache_meta')
|
||||
with open(cache_meta_file, 'w+') as f, self.cache_lock:
|
||||
f.write(
|
||||
json.dumps(self.cache,
|
||||
indent=2,
|
||||
cls=CacheManager.CacheEncoder))
|
||||
json.dumps(
|
||||
self.cache, indent=2, cls=CacheManager.CacheEncoder))
|
||||
|
||||
def save_file(self, absolute_path: Path, data: bytes):
|
||||
# Make the necessary directories and write to file.
|
||||
@@ -201,10 +200,11 @@ class CacheManager(metaclass=Singleton):
|
||||
"""
|
||||
Determine where to temporarily put the file as it is downloading.
|
||||
"""
|
||||
xdg_cache_home = (os.environ.get('XDG_CACHE_HOME')
|
||||
or os.path.expanduser('~/.cache'))
|
||||
return Path(xdg_cache_home).joinpath('libremsonic',
|
||||
*relative_paths)
|
||||
xdg_cache_home = (
|
||||
os.environ.get('XDG_CACHE_HOME')
|
||||
or os.path.expanduser('~/.cache'))
|
||||
return Path(xdg_cache_home).joinpath(
|
||||
'libremsonic', *relative_paths)
|
||||
|
||||
def return_cached_or_download(
|
||||
self,
|
||||
@@ -212,10 +212,14 @@ class CacheManager(metaclass=Singleton):
|
||||
download_fn: Callable[[], bytes],
|
||||
before_download: Callable[[], None] = lambda: None,
|
||||
force: bool = False,
|
||||
allow_download: bool = True,
|
||||
):
|
||||
abs_path = self.calculate_abs_path(relative_path)
|
||||
download_path = self.calculate_download_path(relative_path)
|
||||
if not abs_path.exists() or force:
|
||||
if not allow_download:
|
||||
return None
|
||||
|
||||
before_download()
|
||||
resource_downloading = False
|
||||
with self.download_set_lock:
|
||||
@@ -337,8 +341,9 @@ class CacheManager(metaclass=Singleton):
|
||||
) -> Future:
|
||||
def do_get_artists() -> List[Union[Artist, ArtistID3]]:
|
||||
cache_name = self.id3ify('artists')
|
||||
server_fn = (self.server.get_artists if self.browse_by_tags
|
||||
else self.server.get_indexes)
|
||||
server_fn = (
|
||||
self.server.get_artists
|
||||
if self.browse_by_tags else self.server.get_indexes)
|
||||
|
||||
if not self.cache.get(cache_name) or force:
|
||||
before_download()
|
||||
@@ -365,8 +370,9 @@ class CacheManager(metaclass=Singleton):
|
||||
) -> Future:
|
||||
def do_get_artist() -> Union[ArtistWithAlbumsID3, Child]:
|
||||
cache_name = self.id3ify('artist_details')
|
||||
server_fn = (self.server.get_artist if self.browse_by_tags else
|
||||
self.server.get_music_directory)
|
||||
server_fn = (
|
||||
self.server.get_artist if self.browse_by_tags else
|
||||
self.server.get_music_directory)
|
||||
|
||||
if artist_id not in self.cache.get(cache_name, {}) or force:
|
||||
before_download()
|
||||
@@ -389,9 +395,9 @@ class CacheManager(metaclass=Singleton):
|
||||
) -> Future:
|
||||
def do_get_artist_info() -> Union[ArtistInfo, ArtistInfo2]:
|
||||
cache_name = self.id3ify('artist_infos')
|
||||
server_fn = (self.server.get_artist_info2
|
||||
if self.browse_by_tags else
|
||||
self.server.get_artist_info)
|
||||
server_fn = (
|
||||
self.server.get_artist_info2
|
||||
if self.browse_by_tags else self.server.get_artist_info)
|
||||
|
||||
if artist_id not in self.cache.get(cache_name, {}) or force:
|
||||
before_download()
|
||||
@@ -452,8 +458,9 @@ class CacheManager(metaclass=Singleton):
|
||||
) -> Future:
|
||||
def do_get_albums() -> List[Child]:
|
||||
cache_name = self.id3ify('albums')
|
||||
server_fn = (self.server.get_album_list2 if self.browse_by_tags
|
||||
else self.server.get_album_list)
|
||||
server_fn = (
|
||||
self.server.get_album_list2
|
||||
if self.browse_by_tags else self.server.get_album_list)
|
||||
|
||||
# TODO cache per type.
|
||||
# TODO handle random.
|
||||
@@ -478,8 +485,9 @@ class CacheManager(metaclass=Singleton):
|
||||
) -> Future:
|
||||
def do_get_album() -> Union[AlbumWithSongsID3, Child]:
|
||||
cache_name = self.id3ify('album_details')
|
||||
server_fn = (self.server.get_album if self.browse_by_tags else
|
||||
self.server.get_music_directory)
|
||||
server_fn = (
|
||||
self.server.get_album if self.browse_by_tags else
|
||||
self.server.get_music_directory)
|
||||
|
||||
if album_id not in self.cache.get(cache_name, {}) or force:
|
||||
before_download()
|
||||
@@ -554,6 +562,7 @@ class CacheManager(metaclass=Singleton):
|
||||
before_download: Callable[[], None] = lambda: None,
|
||||
size: Union[str, int] = 200,
|
||||
force: bool = False,
|
||||
allow_download: bool = True,
|
||||
) -> Future:
|
||||
def do_get_cover_art_filename() -> str:
|
||||
tag = 'tag_' if self.browse_by_tags else ''
|
||||
@@ -562,6 +571,7 @@ class CacheManager(metaclass=Singleton):
|
||||
lambda: self.server.get_cover_art(id, str(size)),
|
||||
before_download=before_download,
|
||||
force=force,
|
||||
allow_download=allow_download,
|
||||
)
|
||||
|
||||
return CacheManager.executor.submit(do_get_cover_art_filename)
|
||||
|
@@ -70,6 +70,7 @@ class AppConfiguration:
|
||||
and self._cache_location != ''):
|
||||
return self._cache_location
|
||||
else:
|
||||
default_cache_location = (os.environ.get('XDG_DATA_HOME')
|
||||
or os.path.expanduser('~/.local/share'))
|
||||
default_cache_location = (
|
||||
os.environ.get('XDG_DATA_HOME')
|
||||
or os.path.expanduser('~/.local/share'))
|
||||
return os.path.join(default_cache_location, 'libremsonic')
|
||||
|
354
libremsonic/dbus_manager.py
Normal file
354
libremsonic/dbus_manager.py
Normal file
@@ -0,0 +1,354 @@
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from deepdiff import DeepDiff
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
from .state_manager import RepeatType
|
||||
from .cache_manager import CacheManager
|
||||
from .server.api_objects import Child
|
||||
|
||||
|
||||
def dbus_propagate(param_self=None):
|
||||
"""
|
||||
Wraps a function which causes changes to DBus properties.
|
||||
"""
|
||||
def decorator(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(*args):
|
||||
function(*args)
|
||||
(param_self or args[0]).dbus_manager.property_diff()
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class DBusManager:
|
||||
second_microsecond_conversion = 1000000
|
||||
|
||||
current_state = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connection,
|
||||
do_on_method_call,
|
||||
on_set_property,
|
||||
get_state_and_player,
|
||||
):
|
||||
self.get_state_and_player = get_state_and_player
|
||||
self.do_on_method_call = do_on_method_call
|
||||
self.on_set_property = on_set_property
|
||||
self.connection = connection
|
||||
|
||||
def dbus_name_acquired(connection, name):
|
||||
specs = [
|
||||
'org.mpris.MediaPlayer2.xml',
|
||||
'org.mpris.MediaPlayer2.Player.xml',
|
||||
'org.mpris.MediaPlayer2.Playlists.xml',
|
||||
'org.mpris.MediaPlayer2.TrackList.xml',
|
||||
]
|
||||
for spec in specs:
|
||||
spec_path = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
f'ui/mpris_specs/{spec}',
|
||||
)
|
||||
with open(spec_path) as f:
|
||||
node_info = Gio.DBusNodeInfo.new_for_xml(f.read())
|
||||
|
||||
connection.register_object(
|
||||
'/org/mpris/MediaPlayer2',
|
||||
node_info.interfaces[0],
|
||||
self.on_method_call,
|
||||
self.on_get_property,
|
||||
self.on_set_property,
|
||||
)
|
||||
|
||||
# TODO: I have no idea what to do here.
|
||||
def dbus_name_lost(*args):
|
||||
pass
|
||||
|
||||
self.bus_number = Gio.bus_own_name_on_connection(
|
||||
connection,
|
||||
'org.mpris.MediaPlayer2.libremsonic',
|
||||
Gio.BusNameOwnerFlags.NONE,
|
||||
dbus_name_acquired,
|
||||
dbus_name_lost,
|
||||
)
|
||||
|
||||
def shutdown(self):
|
||||
Gio.bus_unown_name(self.bus_number)
|
||||
|
||||
def on_get_property(
|
||||
self,
|
||||
connection,
|
||||
sender,
|
||||
path,
|
||||
interface,
|
||||
property_name,
|
||||
):
|
||||
value = self.property_dict().get(interface, {}).get(property_name)
|
||||
return DBusManager.to_variant(value)
|
||||
|
||||
def on_method_call(
|
||||
self,
|
||||
connection,
|
||||
sender,
|
||||
path,
|
||||
interface,
|
||||
method,
|
||||
params,
|
||||
invocation,
|
||||
):
|
||||
# TODO I don't even know if this works.
|
||||
if interface == 'org.freedesktop.DBus.Properties':
|
||||
if method == 'Get':
|
||||
invocation.return_value(
|
||||
self.on_get_property(
|
||||
connection, sender, path, interface, *params))
|
||||
elif method == 'Set':
|
||||
self.on_set_property(
|
||||
connection, sender, path, interface, *params)
|
||||
elif method == 'GetAll':
|
||||
all_properties = {
|
||||
k: DBusManager.to_variant(v)
|
||||
for k, v in self.property_dict()[interface].items()
|
||||
}
|
||||
invocation.return_value(
|
||||
GLib.Variant('(a{sv})', (all_properties, )))
|
||||
|
||||
return
|
||||
self.do_on_method_call(
|
||||
connection,
|
||||
sender,
|
||||
path,
|
||||
interface,
|
||||
method,
|
||||
params,
|
||||
invocation,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def to_variant(value):
|
||||
if callable(value):
|
||||
return DBusManager.to_variant(value())
|
||||
|
||||
if isinstance(value, GLib.Variant):
|
||||
return value
|
||||
|
||||
if type(value) == tuple:
|
||||
return GLib.Variant(*value)
|
||||
|
||||
if type(value) == dict:
|
||||
return GLib.Variant(
|
||||
'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(type(value))
|
||||
if not variant_type:
|
||||
return value
|
||||
return GLib.Variant(variant_type, value)
|
||||
|
||||
def property_dict(self):
|
||||
state, player = self.get_state_and_player()
|
||||
has_current_song = state.current_song is not None
|
||||
has_next_song = False
|
||||
if state.repeat_type in (RepeatType.REPEAT_QUEUE,
|
||||
RepeatType.REPEAT_SONG):
|
||||
has_next_song = True
|
||||
elif has_current_song and state.current_song.id in state.play_queue:
|
||||
current = state.play_queue.index(state.current_song.id)
|
||||
has_next_song = current < len(state.play_queue) - 1
|
||||
|
||||
if state.active_playlist_id is None:
|
||||
active_playlist = (False, GLib.Variant('(oss)', ('/', '', '')))
|
||||
else:
|
||||
playlist = CacheManager.get_playlist(
|
||||
state.active_playlist_id).result()
|
||||
active_playlist = (
|
||||
True,
|
||||
GLib.Variant(
|
||||
'(oss)',
|
||||
(
|
||||
'/playlist/' + playlist.id,
|
||||
playlist.name,
|
||||
CacheManager.get_cover_art_filename(
|
||||
playlist.coverArt,
|
||||
allow_download=False,
|
||||
).result() or '',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
'org.mpris.MediaPlayer2': {
|
||||
'CanQuit': True,
|
||||
'CanRaise': True,
|
||||
'HasTrackList': True,
|
||||
'Identity': 'Libremsonic',
|
||||
# TODO should implement in #29
|
||||
'DesktopEntry': 'foo',
|
||||
'SupportedUriSchemes': [],
|
||||
'SupportedMimeTypes': [],
|
||||
},
|
||||
'org.mpris.MediaPlayer2.Player': {
|
||||
'PlaybackStatus': {
|
||||
(False, False): 'Stopped',
|
||||
(False, True): 'Stopped',
|
||||
(True, False): 'Paused',
|
||||
(True, True): 'Playing',
|
||||
}[player.song_loaded, state.playing],
|
||||
'LoopStatus':
|
||||
state.repeat_type.as_mpris_loop_status(),
|
||||
'Rate':
|
||||
1.0,
|
||||
'Shuffle':
|
||||
state.shuffle_on,
|
||||
'Metadata':
|
||||
self.get_mpris_metadata(state.current_song)
|
||||
if state.current_song else {},
|
||||
'Volume':
|
||||
0.0 if state.is_muted else state.volume / 100,
|
||||
'Position': (
|
||||
'x',
|
||||
int(
|
||||
max(state.song_progress or 0, 0)
|
||||
* self.second_microsecond_conversion),
|
||||
),
|
||||
'MinimumRate':
|
||||
1.0,
|
||||
'MaximumRate':
|
||||
1.0,
|
||||
'CanGoNext':
|
||||
has_current_song and has_next_song,
|
||||
'CanGoPrevious':
|
||||
has_current_song,
|
||||
'CanPlay':
|
||||
True,
|
||||
'CanPause':
|
||||
True,
|
||||
'CanSeek':
|
||||
True,
|
||||
'CanControl':
|
||||
True,
|
||||
},
|
||||
'org.mpris.MediaPlayer2.TrackList': {
|
||||
'Tracks': ['/song/' + i for i in state.play_queue],
|
||||
'CanEditTracks': False,
|
||||
},
|
||||
'org.mpris.MediaPlayer2.Playlists': {
|
||||
# TODO this may do a network request. This really is a case for
|
||||
# doing the whole thing with caching some data beforehand.
|
||||
'PlaylistCount': len(CacheManager.get_playlists().result()),
|
||||
'Orderings': ['Alphabetical', 'Created', 'Modified'],
|
||||
'ActivePlaylist': ('(b(oss))', active_playlist),
|
||||
},
|
||||
}
|
||||
|
||||
def get_mpris_metadata(self, song: Child):
|
||||
duration = (
|
||||
'x',
|
||||
(song.duration or 0) * self.second_microsecond_conversion,
|
||||
)
|
||||
return {
|
||||
'mpris:trackid': '/song/' + song.id,
|
||||
'mpris:length': duration,
|
||||
'mpris:artUrl': CacheManager.get_cover_art_url(song.id, 1000),
|
||||
'xesam:album': song.album,
|
||||
'xesam:albumArtist': [song.artist],
|
||||
'xesam:artist': [song.artist],
|
||||
'xesam:title': song.title,
|
||||
}
|
||||
|
||||
diff_parse_re = re.compile(r"root\['(.*?)'\]\['(.*?)'\](?:\[.*\])?")
|
||||
|
||||
def property_diff(self):
|
||||
new_property_dict = self.property_dict()
|
||||
diff = DeepDiff(self.current_state, new_property_dict)
|
||||
|
||||
changes = defaultdict(dict)
|
||||
|
||||
for path, change in diff.get('values_changed', {}).items():
|
||||
interface, property_name = self.diff_parse_re.match(path).groups()
|
||||
changes[interface][property_name] = change['new_value']
|
||||
|
||||
if diff.get('dictionary_item_added'):
|
||||
changes = new_property_dict
|
||||
|
||||
for interface, changed_props in changes.items():
|
||||
# If the metadata has changed, just make the entire Metadata object
|
||||
# part of the update.
|
||||
if 'Metadata' in changed_props.keys():
|
||||
changed_props['Metadata'] = new_property_dict[interface][
|
||||
'Metadata']
|
||||
|
||||
# Special handling for when the position changes (a seek).
|
||||
# Technically, I'm sending this signal too often, but I don't think
|
||||
# it really matters.
|
||||
if (interface == 'org.mpris.MediaPlayer2.Player'
|
||||
and 'Position' in changed_props):
|
||||
self.connection.emit_signal(
|
||||
None,
|
||||
'/org/mpris/MediaPlayer2',
|
||||
interface,
|
||||
'Seeked',
|
||||
GLib.Variant('(x)', (changed_props['Position'][1], )),
|
||||
)
|
||||
|
||||
# Do not emit the property change.
|
||||
del changed_props['Position']
|
||||
|
||||
# Special handling for when the track list changes.
|
||||
# Technically, I'm supposed to use `TrackAdded` and `TrackRemoved`
|
||||
# signals when minor changes occur, but the docs also say that:
|
||||
#
|
||||
# > It is left up to the implementation to decide when a change to
|
||||
# > the track list is invasive enough that this signal should be
|
||||
# > emitted instead of a series of TrackAdded and TrackRemoved
|
||||
# > signals.
|
||||
#
|
||||
# So I think that any change is invasive enough that I should use
|
||||
# this signal.
|
||||
if (interface == 'org.mpris.MediaPlayer2.TrackList'
|
||||
and 'Tracks' in changed_props):
|
||||
track_list = changed_props['Tracks']
|
||||
current_track = (
|
||||
new_property_dict['org.mpris.MediaPlayer2.Player']
|
||||
['Metadata']['mpris:trackid'])
|
||||
self.connection.emit_signal(
|
||||
None,
|
||||
'/org/mpris/MediaPlayer2',
|
||||
interface,
|
||||
'TrackListReplaced',
|
||||
GLib.Variant('(aoo)', (track_list, current_track)),
|
||||
)
|
||||
|
||||
self.connection.emit_signal(
|
||||
None,
|
||||
'/org/mpris/MediaPlayer2',
|
||||
'org.freedesktop.DBus.Properties',
|
||||
'PropertiesChanged',
|
||||
GLib.Variant(
|
||||
'(sa{sv}as)', (
|
||||
interface,
|
||||
{
|
||||
k: DBusManager.to_variant(v)
|
||||
for k, v in changed_props.items()
|
||||
},
|
||||
[],
|
||||
)),
|
||||
)
|
||||
|
||||
# Update state for next diff.
|
||||
self.current_state = new_property_dict
|
@@ -24,9 +24,10 @@ class APIObject:
|
||||
|
||||
annotations: Dict[str, Any] = self.get('__annotations__', {})
|
||||
typename = type(self).__name__
|
||||
fieldstr = ' '.join([
|
||||
f'{field}={getattr(self, field)!r}'
|
||||
for field in annotations.keys()
|
||||
if hasattr(self, field) and getattr(self, field) is not None
|
||||
])
|
||||
fieldstr = ' '.join(
|
||||
[
|
||||
f'{field}={getattr(self, field)!r}'
|
||||
for field in annotations.keys()
|
||||
if hasattr(self, field) and getattr(self, field) is not None
|
||||
])
|
||||
return f'<{typename} {fieldstr}>'
|
||||
|
@@ -164,9 +164,10 @@ class Server:
|
||||
:param if_modified_since: If specified, only return a result if the
|
||||
artist collection has changed since the given time.
|
||||
"""
|
||||
result = self._get_json(self._make_url('getIndexes'),
|
||||
musicFolderId=music_folder_id,
|
||||
ifModifiedSince=if_modified_since)
|
||||
result = self._get_json(
|
||||
self._make_url('getIndexes'),
|
||||
musicFolderId=music_folder_id,
|
||||
ifModifiedSince=if_modified_since)
|
||||
return result.indexes
|
||||
|
||||
def get_music_directory(self, dir_id) -> Directory:
|
||||
@@ -177,8 +178,8 @@ class Server:
|
||||
:param dir_id: A string which uniquely identifies the music folder.
|
||||
Obtained by calls to ``getIndexes`` or ``getMusicDirectory``.
|
||||
"""
|
||||
result = self._get_json(self._make_url('getMusicDirectory'),
|
||||
id=str(dir_id))
|
||||
result = self._get_json(
|
||||
self._make_url('getMusicDirectory'), id=str(dir_id))
|
||||
return result.directory
|
||||
|
||||
def get_genres(self) -> Genres:
|
||||
@@ -195,8 +196,8 @@ class Server:
|
||||
:param music_folder_id: If specified, only return artists in the music
|
||||
folder with the given ID. See ``getMusicFolders``.
|
||||
"""
|
||||
result = self._get_json(self._make_url('getArtists'),
|
||||
musicFolderId=music_folder_id)
|
||||
result = self._get_json(
|
||||
self._make_url('getArtists'), musicFolderId=music_folder_id)
|
||||
return result.artists
|
||||
|
||||
def get_artist(self, artist_id: int) -> ArtistWithAlbumsID3:
|
||||
@@ -666,8 +667,8 @@ class Server:
|
||||
user rather than for the authenticated user. The authenticated user
|
||||
must have admin role if this parameter is used.
|
||||
"""
|
||||
result = self._get_json(self._make_url('getPlaylists'),
|
||||
username=username)
|
||||
result = self._get_json(
|
||||
self._make_url('getPlaylists'), username=username)
|
||||
return result.playlists
|
||||
|
||||
def get_playlist(self, id: int = None) -> PlaylistWithSongs:
|
||||
@@ -814,9 +815,8 @@ class Server:
|
||||
:param id: The ID of a song, album or artist.
|
||||
:param size: If specified, scale image to this size.
|
||||
"""
|
||||
return self.do_download(self._make_url('getCoverArt'),
|
||||
id=id,
|
||||
size=size)
|
||||
return self.do_download(
|
||||
self._make_url('getCoverArt'), id=id, size=size)
|
||||
|
||||
def get_cover_art_url(self, id: str, size: str = None):
|
||||
"""
|
||||
@@ -914,9 +914,8 @@ class Server:
|
||||
:param rating: The rating between 1 and 5 (inclusive), or 0 to remove
|
||||
the rating.
|
||||
"""
|
||||
return self._get_json(self._make_url('setRating'),
|
||||
id=id,
|
||||
rating=rating)
|
||||
return self._get_json(
|
||||
self._make_url('setRating'), id=id, rating=rating)
|
||||
|
||||
def scrobble(
|
||||
self,
|
||||
@@ -1076,8 +1075,8 @@ class Server:
|
||||
|
||||
:param id: The ID for the station.
|
||||
"""
|
||||
return self._get_json(self._make_url('deleteInternetRadioStation'),
|
||||
id=id)
|
||||
return self._get_json(
|
||||
self._make_url('deleteInternetRadioStation'), id=id)
|
||||
|
||||
def get_user(self, username: str) -> User:
|
||||
"""
|
||||
|
@@ -23,6 +23,17 @@ class RepeatType(Enum):
|
||||
][self.value]
|
||||
return 'media-playlist-' + icon_name
|
||||
|
||||
def as_mpris_loop_status(self):
|
||||
return ['None', 'Playlist', 'Track'][self.value]
|
||||
|
||||
@staticmethod
|
||||
def from_mpris_loop_status(loop_status):
|
||||
return {
|
||||
'None': RepeatType.NO_REPEAT,
|
||||
'Track': RepeatType.REPEAT_SONG,
|
||||
'Playlist': RepeatType.REPEAT_QUEUE,
|
||||
}[loop_status]
|
||||
|
||||
|
||||
class ApplicationState:
|
||||
"""
|
||||
@@ -55,11 +66,13 @@ class ApplicationState:
|
||||
selected_artist_id: str = None
|
||||
selected_playlist_id: str = None
|
||||
current_album_sort: str = 'random'
|
||||
active_playlist_id: str = None
|
||||
|
||||
def to_json(self):
|
||||
current_song = (self.current_song.id if
|
||||
(hasattr(self, 'current_song')
|
||||
and self.current_song is not None) else None)
|
||||
current_song = (
|
||||
self.current_song.id if
|
||||
(hasattr(self, 'current_song')
|
||||
and self.current_song is not None) else None)
|
||||
return {
|
||||
'current_song': current_song,
|
||||
'play_queue': getattr(self, 'play_queue', None),
|
||||
@@ -74,9 +87,10 @@ class ApplicationState:
|
||||
'current_tab': getattr(self, 'current_tab', 'albums'),
|
||||
'selected_album_id': getattr(self, 'selected_album_id', None),
|
||||
'selected_artist_id': getattr(self, 'selected_artist_id', None),
|
||||
'selected_playlist_id': getattr(self, 'selected_playlist_id',
|
||||
None),
|
||||
'selected_playlist_id':
|
||||
getattr(self, 'selected_playlist_id', None),
|
||||
'current_album_sort': getattr(self, 'current_album_sort', None),
|
||||
'active_playlist_id': getattr(self, 'active_playlist_id', None),
|
||||
}
|
||||
|
||||
def load_from_json(self, json_object):
|
||||
@@ -91,18 +105,19 @@ class ApplicationState:
|
||||
self.old_play_queue = json_object.get('old_play_queue') or []
|
||||
self._volume = json_object.get('_volume') or {'this device': 100}
|
||||
self.is_muted = json_object.get('is_muted') or False
|
||||
self.repeat_type = (RepeatType(json_object.get('repeat_type'))
|
||||
or RepeatType.NO_REPEAT)
|
||||
self.repeat_type = (
|
||||
RepeatType(json_object.get('repeat_type')) or RepeatType.NO_REPEAT)
|
||||
self.shuffle_on = json_object.get('shuffle_on', False)
|
||||
self.song_progress = json_object.get('song_progress', 0.0)
|
||||
self.current_device = json_object.get('current_device', 'this device')
|
||||
self.current_tab = json_object.get('current_tab', 'albums')
|
||||
self.selected_album_id = json_object.get('selected_album_id', None)
|
||||
self.selected_artist_id = json_object.get('selected_artist_id', None)
|
||||
self.selected_playlist_id = json_object.get('selected_playlist_id',
|
||||
None)
|
||||
self.current_album_sort = json_object.get('current_album_sort',
|
||||
'random')
|
||||
self.selected_playlist_id = json_object.get(
|
||||
'selected_playlist_id', None)
|
||||
self.current_album_sort = json_object.get(
|
||||
'current_album_sort', 'random')
|
||||
self.active_playlist_id = json_object.get('active_playlist_id', None)
|
||||
|
||||
def load(self):
|
||||
self.config = self.get_config(self.config_file)
|
||||
@@ -130,8 +145,8 @@ class ApplicationState:
|
||||
|
||||
# Save the config
|
||||
with open(self.config_file, 'w+') as f:
|
||||
f.write(json.dumps(self.config.to_json(), indent=2,
|
||||
sort_keys=True))
|
||||
f.write(
|
||||
json.dumps(self.config.to_json(), indent=2, sort_keys=True))
|
||||
|
||||
# Save the state
|
||||
with open(self.state_filename, 'w+') as f:
|
||||
@@ -151,8 +166,8 @@ class ApplicationState:
|
||||
def state_filename(self):
|
||||
# TODO: this should probably not be stored in ~/.cache. I blow this
|
||||
# away too often...
|
||||
state_filename = (os.environ.get('XDG_CACHE_HOME')
|
||||
or os.path.expanduser('~/.cache'))
|
||||
state_filename = (
|
||||
os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'))
|
||||
return os.path.join(state_filename, 'libremsonic/state.yaml')
|
||||
|
||||
@property
|
||||
|
@@ -244,8 +244,8 @@ class AlbumsGrid(CoverArtGrid):
|
||||
# Override Methods
|
||||
# =========================================================================
|
||||
def get_header_text(self, item: AlbumModel) -> str:
|
||||
return (item.album.title
|
||||
if type(item.album) == Child else item.album.name)
|
||||
return (
|
||||
item.album.title if type(item.album) == Child else item.album.name)
|
||||
|
||||
def get_info_text(self, item: AlbumModel) -> Optional[str]:
|
||||
return util.dot_join(item.album.artist, item.album.year)
|
||||
|
@@ -92,8 +92,9 @@ class ArtistList(Gtk.Box):
|
||||
|
||||
album_count = model.album_count
|
||||
if album_count:
|
||||
label_text.append('{} {}'.format(
|
||||
album_count, util.pluralize('album', album_count)))
|
||||
label_text.append(
|
||||
'{} {}'.format(
|
||||
album_count, util.pluralize('album', album_count)))
|
||||
|
||||
row = Gtk.ListBoxRow(
|
||||
action_name='app.go-to-artist',
|
||||
@@ -190,15 +191,15 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
|
||||
view_refresh_button = IconButton('view-refresh-symbolic')
|
||||
view_refresh_button.connect('clicked', self.on_view_refresh_click)
|
||||
self.artist_action_buttons.pack_end(view_refresh_button, False, False,
|
||||
5)
|
||||
self.artist_action_buttons.pack_end(
|
||||
view_refresh_button, False, False, 5)
|
||||
|
||||
download_all_btn = IconButton('folder-download-symbolic')
|
||||
download_all_btn.connect('clicked', self.on_download_all_click)
|
||||
self.artist_action_buttons.pack_end(download_all_btn, False, False, 5)
|
||||
|
||||
artist_details_box.pack_start(self.artist_action_buttons, False, False,
|
||||
5)
|
||||
artist_details_box.pack_start(
|
||||
self.artist_action_buttons, False, False, 5)
|
||||
|
||||
artist_details_box.pack_start(Gtk.Box(), True, False, 0)
|
||||
|
||||
@@ -208,8 +209,8 @@ class ArtistDetailPanel(Gtk.Box):
|
||||
self.artist_name = self.make_label(name='artist-name')
|
||||
artist_details_box.add(self.artist_name)
|
||||
|
||||
self.artist_bio = self.make_label(name='artist-bio',
|
||||
justify=Gtk.Justification.LEFT)
|
||||
self.artist_bio = self.make_label(
|
||||
name='artist-bio', justify=Gtk.Justification.LEFT)
|
||||
self.artist_bio.set_line_wrap(True)
|
||||
artist_details_box.add(self.artist_bio)
|
||||
|
||||
|
@@ -44,8 +44,8 @@ class AlbumWithSongs(Gtk.Box):
|
||||
spinner_name='artist-artwork-spinner',
|
||||
)
|
||||
# Account for 10px margin on all sides with "+ 20".
|
||||
artist_artwork.set_size_request(cover_art_size + 20,
|
||||
cover_art_size + 20)
|
||||
artist_artwork.set_size_request(
|
||||
cover_art_size + 20, cover_art_size + 20)
|
||||
box.pack_start(artist_artwork, False, False, 0)
|
||||
box.pack_start(Gtk.Box(), True, True, 0)
|
||||
self.pack_start(box, False, False, 0)
|
||||
@@ -75,30 +75,30 @@ class AlbumWithSongs(Gtk.Box):
|
||||
ellipsize=Pango.EllipsizeMode.END,
|
||||
))
|
||||
|
||||
self.play_btn = IconButton('media-playback-start-symbolic',
|
||||
sensitive=False)
|
||||
self.play_btn = IconButton(
|
||||
'media-playback-start-symbolic', sensitive=False)
|
||||
self.play_btn.connect('clicked', self.play_btn_clicked)
|
||||
album_title_and_buttons.pack_start(self.play_btn, False, False, 5)
|
||||
|
||||
self.shuffle_btn = IconButton('media-playlist-shuffle-symbolic',
|
||||
sensitive=False)
|
||||
self.shuffle_btn = IconButton(
|
||||
'media-playlist-shuffle-symbolic', sensitive=False)
|
||||
self.shuffle_btn.connect('clicked', self.shuffle_btn_clicked)
|
||||
album_title_and_buttons.pack_start(self.shuffle_btn, False, False, 5)
|
||||
|
||||
self.play_next_btn = IconButton('go-top-symbolic',
|
||||
action_name='app.play-next')
|
||||
self.play_next_btn = IconButton(
|
||||
'go-top-symbolic', action_name='app.play-next')
|
||||
album_title_and_buttons.pack_start(self.play_next_btn, False, False, 5)
|
||||
|
||||
self.add_to_queue_btn = IconButton('go-jump-symbolic',
|
||||
action_name='app.add-to-queue')
|
||||
album_title_and_buttons.pack_start(self.add_to_queue_btn, False, False,
|
||||
5)
|
||||
self.add_to_queue_btn = IconButton(
|
||||
'go-jump-symbolic', action_name='app.add-to-queue')
|
||||
album_title_and_buttons.pack_start(
|
||||
self.add_to_queue_btn, False, False, 5)
|
||||
|
||||
self.download_all_btn = IconButton('folder-download-symbolic',
|
||||
sensitive=False)
|
||||
self.download_all_btn = IconButton(
|
||||
'folder-download-symbolic', sensitive=False)
|
||||
self.download_all_btn.connect('clicked', self.on_download_all_click)
|
||||
album_title_and_buttons.pack_end(self.download_all_btn, False, False,
|
||||
5)
|
||||
album_title_and_buttons.pack_end(
|
||||
self.download_all_btn, False, False, 5)
|
||||
|
||||
album_details.add(album_title_and_buttons)
|
||||
|
||||
@@ -169,10 +169,10 @@ class AlbumWithSongs(Gtk.Box):
|
||||
create_column('DURATION', 2, align=1, width=40))
|
||||
|
||||
self.album_songs.connect('row-activated', self.on_song_activated)
|
||||
self.album_songs.connect('button-press-event',
|
||||
self.on_song_button_press)
|
||||
self.album_songs.get_selection().connect('changed',
|
||||
self.on_song_selection_change)
|
||||
self.album_songs.connect(
|
||||
'button-press-event', self.on_song_button_press)
|
||||
self.album_songs.get_selection().connect(
|
||||
'changed', self.on_song_selection_change)
|
||||
album_details.add(self.album_songs)
|
||||
|
||||
self.pack_end(album_details, True, True, 0)
|
||||
@@ -188,8 +188,9 @@ class AlbumWithSongs(Gtk.Box):
|
||||
def on_song_activated(self, treeview, idx, column):
|
||||
# The song ID is in the last column of the model.
|
||||
song_id = self.album_song_store[idx][-1]
|
||||
self.emit('song-clicked', song_id,
|
||||
[m[-1] for m in self.album_song_store], {})
|
||||
self.emit(
|
||||
'song-clicked', song_id, [m[-1] for m in self.album_song_store],
|
||||
{})
|
||||
|
||||
def on_song_button_press(self, tree, event):
|
||||
if event.button == 3: # Right click
|
||||
@@ -273,19 +274,22 @@ class AlbumWithSongs(Gtk.Box):
|
||||
album: Union[AlbumWithSongsID3, Child, Directory],
|
||||
state: ApplicationState,
|
||||
):
|
||||
new_store = [[
|
||||
util.get_cached_status_icon(CacheManager.get_cached_status(song)),
|
||||
util.esc(song.title),
|
||||
util.format_song_duration(song.duration),
|
||||
song.id,
|
||||
] for song in (album.get('child') or album.get('song') or [])]
|
||||
new_store = [
|
||||
[
|
||||
util.get_cached_status_icon(
|
||||
CacheManager.get_cached_status(song)),
|
||||
util.esc(song.title),
|
||||
util.format_song_duration(song.duration),
|
||||
song.id,
|
||||
] for song in (album.get('child') or album.get('song') or [])
|
||||
]
|
||||
|
||||
song_ids = [song[-1] for song in new_store]
|
||||
|
||||
self.play_btn.set_sensitive(True)
|
||||
self.shuffle_btn.set_sensitive(True)
|
||||
self.play_next_btn.set_action_target_value(GLib.Variant(
|
||||
'as', song_ids))
|
||||
self.play_next_btn.set_action_target_value(
|
||||
GLib.Variant('as', song_ids))
|
||||
self.add_to_queue_btn.set_action_target_value(
|
||||
GLib.Variant('as', song_ids))
|
||||
self.download_all_btn.set_sensitive(True)
|
||||
|
@@ -189,9 +189,9 @@ class CoverArtGrid(Gtk.ScrolledWindow):
|
||||
# Determine where the cuttoff is between the top and bottom grids.
|
||||
entries_before_fold = len(self.list_store)
|
||||
if self.selected_list_store_index is not None:
|
||||
entries_before_fold = ((
|
||||
(self.selected_list_store_index // self.items_per_row) + 1)
|
||||
* self.items_per_row)
|
||||
entries_before_fold = (
|
||||
((self.selected_list_store_index // self.items_per_row) + 1)
|
||||
* self.items_per_row)
|
||||
|
||||
if force_reload_from_master:
|
||||
# Just remove everything and re-add all of the items.
|
||||
@@ -268,8 +268,8 @@ class CoverArtGrid(Gtk.ScrolledWindow):
|
||||
# =========================================================================
|
||||
def on_child_activated(self, flowbox, child):
|
||||
click_top = flowbox == self.grid_top
|
||||
selected = (child.get_index() +
|
||||
(0 if click_top else len(self.list_store_top)))
|
||||
selected = (
|
||||
child.get_index() + (0 if click_top else len(self.list_store_top)))
|
||||
|
||||
if selected == self.selected_list_store_index:
|
||||
self.selected_list_store_index = None
|
||||
|
@@ -113,5 +113,4 @@ class EditFormDialog(Gtk.Dialog):
|
||||
Gtk.ResponseType.OK,
|
||||
)
|
||||
|
||||
|
||||
self.show_all()
|
||||
|
@@ -10,12 +10,12 @@ class IconButton(Gtk.Button):
|
||||
relief=False,
|
||||
icon_size=Gtk.IconSize.BUTTON,
|
||||
label=None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
):
|
||||
Gtk.Button.__init__(self, **kwargs)
|
||||
self.icon_size = icon_size
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL,
|
||||
name='icon-button-box')
|
||||
box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, name='icon-button-box')
|
||||
|
||||
self.image = Gtk.Image()
|
||||
self.image.set_from_icon_name(icon_name, self.icon_size)
|
||||
|
@@ -242,8 +242,9 @@ class ChromecastPlayer(Player):
|
||||
return ChromecastPlayer.executor.submit(do_get_chromecasts)
|
||||
|
||||
def set_playing_chromecast(self, uuid):
|
||||
self.chromecast = next(cc for cc in ChromecastPlayer.chromecasts
|
||||
if cc.device.uuid == UUID(uuid))
|
||||
self.chromecast = next(
|
||||
cc for cc in ChromecastPlayer.chromecasts
|
||||
if cc.device.uuid == UUID(uuid))
|
||||
|
||||
self.chromecast.media_controller.register_status_listener(
|
||||
ChromecastPlayer.media_status_listener)
|
||||
|
@@ -104,8 +104,8 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
|
||||
# Server List
|
||||
self.server_list = Gtk.ListBox(activate_on_single_click=False)
|
||||
self.server_list.connect('selected-rows-changed',
|
||||
self.server_list_on_selected_rows_changed)
|
||||
self.server_list.connect(
|
||||
'selected-rows-changed', self.server_list_on_selected_rows_changed)
|
||||
self.server_list.connect('row-activated', self.on_server_list_activate)
|
||||
flowbox.pack_start(self.server_list, True, True, 10)
|
||||
|
||||
@@ -114,18 +114,23 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
# Add all of the buttons to the button box.
|
||||
self.buttons = [
|
||||
# TODO get good icons for these
|
||||
(IconButton('document-edit-symbolic', label='Edit...',
|
||||
relief=True), lambda e: self.on_edit_clicked(e, False),
|
||||
'start', True),
|
||||
(IconButton('list-add', label='Add...', relief=True),
|
||||
lambda e: self.on_edit_clicked(e, True), 'start', False),
|
||||
(IconButton('list-remove', label='Remove',
|
||||
relief=True), self.on_remove_clicked, 'start', True),
|
||||
(IconButton('window-close', label='Close',
|
||||
relief=True), lambda _: self.close(), 'end', False),
|
||||
(IconButton('network-transmit-receive',
|
||||
label='Connect',
|
||||
relief=True), self.on_connect_clicked, 'end', True),
|
||||
(
|
||||
IconButton(
|
||||
'document-edit-symbolic', label='Edit...', relief=True),
|
||||
lambda e: self.on_edit_clicked(e, False), 'start', True),
|
||||
(
|
||||
IconButton('list-add', label='Add...', relief=True),
|
||||
lambda e: self.on_edit_clicked(e, True), 'start', False),
|
||||
(
|
||||
IconButton('list-remove', label='Remove', relief=True),
|
||||
self.on_remove_clicked, 'start', True),
|
||||
(
|
||||
IconButton('window-close', label='Close',
|
||||
relief=True), lambda _: self.close(), 'end', False),
|
||||
(
|
||||
IconButton(
|
||||
'network-transmit-receive', label='Connect',
|
||||
relief=True), self.on_connect_clicked, 'end', True),
|
||||
]
|
||||
for button_cfg in self.buttons:
|
||||
btn, action, pack_end, requires_selection = button_cfg
|
||||
@@ -187,8 +192,8 @@ class ConfigureServersDialog(Gtk.Dialog):
|
||||
dialog = EditServerDialog(self)
|
||||
else:
|
||||
selected_index = self.server_list.get_selected_row().get_index()
|
||||
dialog = EditServerDialog(self,
|
||||
self.server_configs[selected_index])
|
||||
dialog = EditServerDialog(
|
||||
self, self.server_configs[selected_index])
|
||||
|
||||
result = dialog.run()
|
||||
if result == Gtk.ResponseType.OK:
|
||||
|
@@ -118,8 +118,10 @@ class MainWindow(Gtk.ApplicationWindow):
|
||||
|
||||
menu_items = [
|
||||
(None, self.connected_to_label),
|
||||
('app.configure-servers',
|
||||
Gtk.ModelButton(text='Configure Servers')),
|
||||
(
|
||||
'app.configure-servers',
|
||||
Gtk.ModelButton(text='Configure Servers'),
|
||||
),
|
||||
('app.settings', Gtk.ModelButton(text='Settings')),
|
||||
]
|
||||
|
||||
|
672
libremsonic/ui/mpris_specs/org.mpris.MediaPlayer2.Player.xml
Normal file
672
libremsonic/ui/mpris_specs/org.mpris.MediaPlayer2.Player.xml
Normal file
@@ -0,0 +1,672 @@
|
||||
<?xml version="1.0" ?>
|
||||
<node name="/Player_Interface" xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0">
|
||||
<interface name="org.mpris.MediaPlayer2.Player">
|
||||
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
This interface implements the methods for querying and providing basic
|
||||
control over what is currently playing.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
|
||||
<tp:enum name="Playback_Status" tp:name-for-bindings="Playback_Status" type="s">
|
||||
<tp:enumvalue suffix="Playing" value="Playing">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A track is currently playing.</p>
|
||||
</tp:docstring>
|
||||
</tp:enumvalue>
|
||||
<tp:enumvalue suffix="Paused" value="Paused">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A track is currently paused.</p>
|
||||
</tp:docstring>
|
||||
</tp:enumvalue>
|
||||
<tp:enumvalue suffix="Stopped" value="Stopped">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>There is no track currently playing.</p>
|
||||
</tp:docstring>
|
||||
</tp:enumvalue>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A playback state.</p>
|
||||
</tp:docstring>
|
||||
</tp:enum>
|
||||
|
||||
<tp:enum name="Loop_Status" tp:name-for-bindings="Loop_Status" type="s">
|
||||
<tp:enumvalue suffix="None" value="None">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The playback will stop when there are no more tracks to play</p>
|
||||
</tp:docstring>
|
||||
</tp:enumvalue>
|
||||
<tp:enumvalue suffix="Track" value="Track">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The current track will start again from the begining once it has finished playing</p>
|
||||
</tp:docstring>
|
||||
</tp:enumvalue>
|
||||
<tp:enumvalue suffix="Playlist" value="Playlist">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The playback loops through a list of tracks</p>
|
||||
</tp:docstring>
|
||||
</tp:enumvalue>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A repeat / loop status</p>
|
||||
</tp:docstring>
|
||||
</tp:enum>
|
||||
|
||||
<tp:simple-type name="Track_Id" type="o" array-name="Track_Id_List">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Unique track identifier.</p>
|
||||
<p>
|
||||
If the media player implements the TrackList interface and allows
|
||||
the same track to appear multiple times in the tracklist,
|
||||
this must be unique within the scope of the tracklist.
|
||||
</p>
|
||||
<p>
|
||||
Note that this should be a valid D-Bus object id, although clients
|
||||
should not assume that any object is actually exported with any
|
||||
interfaces at that path.
|
||||
</p>
|
||||
<p>
|
||||
Media players may not use any paths starting with
|
||||
<literal>/org/mpris</literal> unless explicitly allowed by this specification.
|
||||
Such paths are intended to have special meaning, such as
|
||||
<literal>/org/mpris/MediaPlayer2/TrackList/NoTrack</literal>
|
||||
to indicate "no track".
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
This is a D-Bus object id as that is the definitive way to have
|
||||
unique identifiers on D-Bus. It also allows for future optional
|
||||
expansions to the specification where tracks are exported to D-Bus
|
||||
with an interface similar to org.gnome.UPnP.MediaItem2.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</tp:simple-type>
|
||||
|
||||
<tp:simple-type name="Playback_Rate" type="d">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A playback rate</p>
|
||||
<p>
|
||||
This is a multiplier, so a value of 0.5 indicates that playback is
|
||||
happening at half speed, while 1.5 means that 1.5 seconds of "track time"
|
||||
is consumed every second.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</tp:simple-type>
|
||||
|
||||
<tp:simple-type name="Volume" type="d">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Audio volume level</p>
|
||||
<ul>
|
||||
<li>0.0 means mute.</li>
|
||||
<li>1.0 is a sensible maximum volume level (ex: 0dB).</li>
|
||||
</ul>
|
||||
<p>
|
||||
Note that the volume may be higher than 1.0, although generally
|
||||
clients should not attempt to set it above 1.0.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</tp:simple-type>
|
||||
|
||||
<tp:simple-type name="Time_In_Us" type="x">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Time in microseconds.</p>
|
||||
</tp:docstring>
|
||||
</tp:simple-type>
|
||||
|
||||
<method name="Next" tp:name-for-bindings="Next">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Skips to the next track in the tracklist.</p>
|
||||
<p>
|
||||
If there is no next track (and endless playback and track
|
||||
repeat are both off), stop playback.
|
||||
</p>
|
||||
<p>If playback is paused or stopped, it remains that way.</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanGoNext</tp:member-ref> is
|
||||
<strong>false</strong>, attempting to call this method should have
|
||||
no effect.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="Previous" tp:name-for-bindings="Previous">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Skips to the previous track in the tracklist.</p>
|
||||
<p>
|
||||
If there is no previous track (and endless playback and track
|
||||
repeat are both off), stop playback.
|
||||
</p>
|
||||
<p>If playback is paused or stopped, it remains that way.</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanGoPrevious</tp:member-ref> is
|
||||
<strong>false</strong>, attempting to call this method should have
|
||||
no effect.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="Pause" tp:name-for-bindings="Pause">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Pauses playback.</p>
|
||||
<p>If playback is already paused, this has no effect.</p>
|
||||
<p>
|
||||
Calling Play after this should cause playback to start again
|
||||
from the same position.
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanPause</tp:member-ref> is
|
||||
<strong>false</strong>, attempting to call this method should have
|
||||
no effect.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="PlayPause" tp:name-for-bindings="PlayPause">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Pauses playback.</p>
|
||||
<p>If playback is already paused, resumes playback.</p>
|
||||
<p>If playback is stopped, starts playback.</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanPause</tp:member-ref> is
|
||||
<strong>false</strong>, attempting to call this method should have
|
||||
no effect and raise an error.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="Stop" tp:name-for-bindings="Stop">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Stops playback.</p>
|
||||
<p>If playback is already stopped, this has no effect.</p>
|
||||
<p>
|
||||
Calling Play after this should cause playback to
|
||||
start again from the beginning of the track.
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanControl</tp:member-ref> is
|
||||
<strong>false</strong>, attempting to call this method should have
|
||||
no effect and raise an error.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="Play" tp:name-for-bindings="Play">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Starts or resumes playback.</p>
|
||||
<p>If already playing, this has no effect.</p>
|
||||
<p>If paused, playback resumes from the current position.</p>
|
||||
<p>If there is no track to play, this has no effect.</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanPlay</tp:member-ref> is
|
||||
<strong>false</strong>, attempting to call this method should have
|
||||
no effect.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="Seek" tp:name-for-bindings="Seek">
|
||||
<arg direction="in" type="x" name="Offset" tp:type="Time_In_Us">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The number of microseconds to seek forward.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
Seeks forward in the current track by the specified number
|
||||
of microseconds.
|
||||
</p>
|
||||
<p>
|
||||
A negative value seeks back. If this would mean seeking
|
||||
back further than the start of the track, the position
|
||||
is set to 0.
|
||||
</p>
|
||||
<p>
|
||||
If the value passed in would mean seeking beyond the end
|
||||
of the track, acts like a call to Next.
|
||||
</p>
|
||||
<p>
|
||||
If the <tp:member-ref>CanSeek</tp:member-ref> property is false,
|
||||
this has no effect.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="SetPosition" tp:name-for-bindings="Set_Position">
|
||||
<arg direction="in" type="o" tp:type="Track_Id" name="TrackId">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The currently playing track's identifier.</p>
|
||||
<p>
|
||||
If this does not match the id of the currently-playing track,
|
||||
the call is ignored as "stale".
|
||||
</p>
|
||||
<p>
|
||||
<literal>/org/mpris/MediaPlayer2/TrackList/NoTrack</literal>
|
||||
is <em>not</em> a valid value for this argument.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<arg direction="in" type="x" tp:type="Time_In_Us" name="Position">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Track position in microseconds.</p>
|
||||
<p>This must be between 0 and <track_length>.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Sets the current track position in microseconds.</p>
|
||||
<p>If the Position argument is less than 0, do nothing.</p>
|
||||
<p>
|
||||
If the Position argument is greater than the track length,
|
||||
do nothing.
|
||||
</p>
|
||||
<p>
|
||||
If the <tp:member-ref>CanSeek</tp:member-ref> property is false,
|
||||
this has no effect.
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
The reason for having this method, rather than making
|
||||
<tp:member-ref>Position</tp:member-ref> writable, is to include
|
||||
the TrackId argument to avoid race conditions where a client tries
|
||||
to seek to a position when the track has already changed.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="OpenUri" tp:name-for-bindings="Open_Uri">
|
||||
<arg direction="in" type="s" tp:type="Uri" name="Uri">
|
||||
<tp:docstring>
|
||||
<p>
|
||||
Uri of the track to load. Its uri scheme should be an element of the
|
||||
<literal>org.mpris.MediaPlayer2.SupportedUriSchemes</literal>
|
||||
property and the mime-type should match one of the elements of the
|
||||
<literal>org.mpris.MediaPlayer2.SupportedMimeTypes</literal>.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Opens the Uri given as an argument</p>
|
||||
<p>If the playback is stopped, starts playing</p>
|
||||
<p>
|
||||
If the uri scheme or the mime-type of the uri to open is not supported,
|
||||
this method does nothing and may raise an error. In particular, if the
|
||||
list of available uri schemes is empty, this method may not be
|
||||
implemented.
|
||||
</p>
|
||||
<p>Clients should not assume that the Uri has been opened as soon as this
|
||||
method returns. They should wait until the mpris:trackid field in the
|
||||
<tp:member-ref>Metadata</tp:member-ref> property changes.
|
||||
</p>
|
||||
<p>
|
||||
If the media player implements the TrackList interface, then the
|
||||
opened track should be made part of the tracklist, the
|
||||
<literal>org.mpris.MediaPlayer2.TrackList.TrackAdded</literal> or
|
||||
<literal>org.mpris.MediaPlayer2.TrackList.TrackListReplaced</literal>
|
||||
signal should be fired, as well as the
|
||||
<literal>org.freedesktop.DBus.Properties.PropertiesChanged</literal>
|
||||
signal on the tracklist interface.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<property name="PlaybackStatus" tp:name-for-bindings="Playback_Status" type="s" tp:type="Playback_Status" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The current playback status.</p>
|
||||
<p>
|
||||
May be "Playing", "Paused" or "Stopped".
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="LoopStatus" type="s" access="readwrite"
|
||||
tp:name-for-bindings="Loop_Status" tp:type="Loop_Status">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<annotation name="org.mpris.MediaPlayer2.property.optional" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The current loop / repeat status</p>
|
||||
<p>May be:
|
||||
<ul>
|
||||
<li>"None" if the playback will stop when there are no more tracks to play</li>
|
||||
<li>"Track" if the current track will start again from the begining once it has finished playing</li>
|
||||
<li>"Playlist" if the playback loops through a list of tracks</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanControl</tp:member-ref> is
|
||||
<strong>false</strong>, attempting to set this property should have
|
||||
no effect and raise an error.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="Rate" tp:name-for-bindings="Rate" type="d" tp:type="Playback_Rate" access="readwrite">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The current playback rate.</p>
|
||||
<p>
|
||||
The value must fall in the range described by
|
||||
<tp:member-ref>MinimumRate</tp:member-ref> and
|
||||
<tp:member-ref>MaximumRate</tp:member-ref>, and must not be 0.0. If
|
||||
playback is paused, the <tp:member-ref>PlaybackStatus</tp:member-ref>
|
||||
property should be used to indicate this. A value of 0.0 should not
|
||||
be set by the client. If it is, the media player should act as
|
||||
though <tp:member-ref>Pause</tp:member-ref> was called.
|
||||
</p>
|
||||
<p>
|
||||
If the media player has no ability to play at speeds other than the
|
||||
normal playback rate, this must still be implemented, and must
|
||||
return 1.0. The <tp:member-ref>MinimumRate</tp:member-ref> and
|
||||
<tp:member-ref>MaximumRate</tp:member-ref> properties must also be
|
||||
set to 1.0.
|
||||
</p>
|
||||
<p>
|
||||
Not all values may be accepted by the media player. It is left to
|
||||
media player implementations to decide how to deal with values they
|
||||
cannot use; they may either ignore them or pick a "best fit" value.
|
||||
Clients are recommended to only use sensible fractions or multiples
|
||||
of 1 (eg: 0.5, 0.25, 1.5, 2.0, etc).
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
This allows clients to display (reasonably) accurate progress bars
|
||||
without having to regularly query the media player for the current
|
||||
position.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="Shuffle" tp:name-for-bindings="Shuffle" type="b" access="readwrite">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<annotation name="org.mpris.MediaPlayer2.property.optional" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
A value of <strong>false</strong> indicates that playback is
|
||||
progressing linearly through a playlist, while <strong>true</strong>
|
||||
means playback is progressing through a playlist in some other order.
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanControl</tp:member-ref> is
|
||||
<strong>false</strong>, attempting to set this property should have
|
||||
no effect and raise an error.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="Metadata" tp:name-for-bindings="Metadata" type="a{sv}" tp:type="Metadata_Map" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The metadata of the current element.</p>
|
||||
<p>
|
||||
If there is a current track, this must have a "mpris:trackid" entry
|
||||
(of D-Bus type "o") at the very least, which contains a D-Bus path that
|
||||
uniquely identifies this track.
|
||||
</p>
|
||||
<p>
|
||||
See the type documentation for more details.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="Volume" type="d" tp:type="Volume" tp:name-for-bindings="Volume" access="readwrite">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true" />
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The volume level.</p>
|
||||
<p>
|
||||
When setting, if a negative value is passed, the volume
|
||||
should be set to 0.0.
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanControl</tp:member-ref> is
|
||||
<strong>false</strong>, attempting to set this property should have
|
||||
no effect and raise an error.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="Position" type="x" tp:type="Time_In_Us" tp:name-for-bindings="Position" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
The current track position in microseconds, between 0 and
|
||||
the 'mpris:length' metadata entry (see Metadata).
|
||||
</p>
|
||||
<p>
|
||||
Note: If the media player allows it, the current playback position
|
||||
can be changed either the SetPosition method or the Seek method on
|
||||
this interface. If this is not the case, the
|
||||
<tp:member-ref>CanSeek</tp:member-ref> property is false, and
|
||||
setting this property has no effect and can raise an error.
|
||||
</p>
|
||||
<p>
|
||||
If the playback progresses in a way that is inconstistant with the
|
||||
<tp:member-ref>Rate</tp:member-ref> property, the
|
||||
<tp:member-ref>Seeked</tp:member-ref> signal is emited.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="MinimumRate" tp:name-for-bindings="Minimum_Rate" type="d" tp:type="Playback_Rate" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
The minimum value which the <tp:member-ref>Rate</tp:member-ref>
|
||||
property can take.
|
||||
Clients should not attempt to set the
|
||||
<tp:member-ref>Rate</tp:member-ref> property below this value.
|
||||
</p>
|
||||
<p>
|
||||
Note that even if this value is 0.0 or negative, clients should
|
||||
not attempt to set the <tp:member-ref>Rate</tp:member-ref> property
|
||||
to 0.0.
|
||||
</p>
|
||||
<p>This value should always be 1.0 or less.</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="MaximumRate" tp:name-for-bindings="Maximum_Rate" type="d" tp:type="Playback_Rate" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
The maximum value which the <tp:member-ref>Rate</tp:member-ref>
|
||||
property can take.
|
||||
Clients should not attempt to set the
|
||||
<tp:member-ref>Rate</tp:member-ref> property above this value.
|
||||
</p>
|
||||
<p>
|
||||
This value should always be 1.0 or greater.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="CanGoNext" tp:name-for-bindings="Can_Go_Next" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
Whether the client can call the <tp:member-ref>Next</tp:member-ref>
|
||||
method on this interface and expect the current track to change.
|
||||
</p>
|
||||
<p>
|
||||
If it is unknown whether a call to <tp:member-ref>Next</tp:member-ref> will
|
||||
be successful (for example, when streaming tracks), this property should
|
||||
be set to <strong>true</strong>.
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanControl</tp:member-ref> is
|
||||
<strong>false</strong>, this property should also be
|
||||
<strong>false</strong>.
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
Even when playback can generally be controlled, there may not
|
||||
always be a next track to move to.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="CanGoPrevious" tp:name-for-bindings="Can_Go_Previous" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
Whether the client can call the
|
||||
<tp:member-ref>Previous</tp:member-ref> method on this interface and
|
||||
expect the current track to change.
|
||||
</p>
|
||||
<p>
|
||||
If it is unknown whether a call to <tp:member-ref>Previous</tp:member-ref>
|
||||
will be successful (for example, when streaming tracks), this property
|
||||
should be set to <strong>true</strong>.
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanControl</tp:member-ref> is
|
||||
<strong>false</strong>, this property should also be
|
||||
<strong>false</strong>.
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
Even when playback can generally be controlled, there may not
|
||||
always be a next previous to move to.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="CanPlay" tp:name-for-bindings="Can_Play" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Whether playback can be started using
|
||||
<tp:member-ref>Play</tp:member-ref> or
|
||||
<tp:member-ref>PlayPause</tp:member-ref>.
|
||||
</p>
|
||||
<p>
|
||||
Note that this is related to whether there is a "current track": the
|
||||
value should not depend on whether the track is currently paused or
|
||||
playing. In fact, if a track is currently playing (and
|
||||
<tp:member-ref>CanControl</tp:member-ref> is <strong>true</strong>),
|
||||
this should be <strong>true</strong>.
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanControl</tp:member-ref> is
|
||||
<strong>false</strong>, this property should also be
|
||||
<strong>false</strong>.
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
Even when playback can generally be controlled, it may not be
|
||||
possible to enter a "playing" state, for example if there is no
|
||||
"current track".
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="CanPause" tp:name-for-bindings="Can_Pause" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Whether playback can be paused using
|
||||
<tp:member-ref>Pause</tp:member-ref> or
|
||||
<tp:member-ref>PlayPause</tp:member-ref>.
|
||||
</p>
|
||||
<p>
|
||||
Note that this is an intrinsic property of the current track: its
|
||||
value should not depend on whether the track is currently paused or
|
||||
playing. In fact, if playback is currently paused (and
|
||||
<tp:member-ref>CanControl</tp:member-ref> is <strong>true</strong>),
|
||||
this should be <strong>true</strong>.
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanControl</tp:member-ref> is
|
||||
<strong>false</strong>, this property should also be
|
||||
<strong>false</strong>.
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
Not all media is pausable: it may not be possible to pause some
|
||||
streamed media, for example.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="CanSeek" tp:name-for-bindings="Can_Seek" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
Whether the client can control the playback position using
|
||||
<tp:member-ref>Seek</tp:member-ref> and
|
||||
<tp:member-ref>SetPosition</tp:member-ref>. This may be different for
|
||||
different tracks.
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanControl</tp:member-ref> is
|
||||
<strong>false</strong>, this property should also be
|
||||
<strong>false</strong>.
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
Not all media is seekable: it may not be possible to seek when
|
||||
playing some streamed media, for example.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="CanControl" tp:name-for-bindings="Can_Control" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Whether the media player may be controlled over this interface.</p>
|
||||
<p>
|
||||
This property is not expected to change, as it describes an intrinsic
|
||||
capability of the implementation.
|
||||
</p>
|
||||
<p>
|
||||
If this is <strong>false</strong>, clients should assume that all
|
||||
properties on this interface are read-only (and will raise errors
|
||||
if writing to them is attempted), no methods are implemented
|
||||
and all other properties starting with "Can" are also
|
||||
<strong>false</strong>.
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
This allows clients to determine whether to present and enable
|
||||
controls to the user in advance of attempting to call methods
|
||||
and write to properties.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<signal name="Seeked" tp:name-for-bindings="Seeked">
|
||||
<arg name="Position" type="x" tp:type="Time_In_Us">
|
||||
<tp:docstring>
|
||||
<p>The new position, in microseconds.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
Indicates that the track position has changed in a way that is
|
||||
inconsistant with the current playing state.
|
||||
</p>
|
||||
<p>When this signal is not received, clients should assume that:</p>
|
||||
<ul>
|
||||
<li>
|
||||
When playing, the position progresses according to the rate property.
|
||||
</li>
|
||||
<li>When paused, it remains constant.</li>
|
||||
</ul>
|
||||
<p>
|
||||
This signal does not need to be emitted when playback starts
|
||||
or when the track changes, unless the track is starting at an
|
||||
unexpected position. An expected position would be the last
|
||||
known one when going from Paused to Playing, and 0 when going from
|
||||
Stopped to Playing.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</signal>
|
||||
|
||||
</interface>
|
||||
</node>
|
||||
<!-- vim:set sw=2 sts=2 et ft=xml: -->
|
250
libremsonic/ui/mpris_specs/org.mpris.MediaPlayer2.Playlists.xml
Normal file
250
libremsonic/ui/mpris_specs/org.mpris.MediaPlayer2.Playlists.xml
Normal file
@@ -0,0 +1,250 @@
|
||||
<?xml version="1.0" ?>
|
||||
<node name="/Playlists_Interface" xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0">
|
||||
<interface name="org.mpris.MediaPlayer2.Playlists">
|
||||
<tp:added version="2.1" />
|
||||
<tp:docstring>
|
||||
<p>Provides access to the media player's playlists.</p>
|
||||
<p>
|
||||
Since D-Bus does not provide an easy way to check for what interfaces
|
||||
are exported on an object, clients should attempt to get one of the
|
||||
properties on this interface to see if it is implemented.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
|
||||
<tp:simple-type name="Playlist_Id" type="o" array-name="Playlist_Id_List">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Unique playlist identifier.</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
Multiple playlists may have the same name.
|
||||
</p>
|
||||
<p>
|
||||
This is a D-Bus object id as that is the definitive way to have
|
||||
unique identifiers on D-Bus. It also allows for future optional
|
||||
expansions to the specification where tracks are exported to D-Bus
|
||||
with an interface similar to org.gnome.UPnP.MediaItem2.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</tp:simple-type>
|
||||
|
||||
<tp:simple-type name="Uri" type="s" array-name="Uri_List">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A URI.</p>
|
||||
</tp:docstring>
|
||||
</tp:simple-type>
|
||||
|
||||
<tp:struct name="Playlist" array-name="Playlist_List">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A data structure describing a playlist.</p>
|
||||
</tp:docstring>
|
||||
<tp:member type="o" tp:type="Playlist_Id" name="Id">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A unique identifier for the playlist.</p>
|
||||
<p>This should remain the same if the playlist is renamed.</p>
|
||||
</tp:docstring>
|
||||
</tp:member>
|
||||
<tp:member type="s" name="Name">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The name of the playlist, typically given by the user.</p>
|
||||
</tp:docstring>
|
||||
</tp:member>
|
||||
<tp:member type="s" tp:type="Uri" name="Icon">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The URI of an (optional) icon.</p>
|
||||
</tp:docstring>
|
||||
</tp:member>
|
||||
</tp:struct>
|
||||
|
||||
<tp:struct name="Maybe_Playlist">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A data structure describing a playlist, or nothing.</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
D-Bus does not (at the time of writing) support a MAYBE type,
|
||||
so we are forced to invent our own.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
<tp:member type="b" name="Valid">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Whether this structure refers to a valid playlist.</p>
|
||||
</tp:docstring>
|
||||
</tp:member>
|
||||
<tp:member type="(oss)" tp:type="Playlist" name="Playlist">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The playlist, providing Valid is true, otherwise undefined.</p>
|
||||
<p>
|
||||
When constructing this type, it should be noted that the playlist
|
||||
ID must be a valid object path, or D-Bus implementations may reject
|
||||
it. This is true even when Valid is false. It is suggested that
|
||||
"/" is used as the playlist ID in this case.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</tp:member>
|
||||
</tp:struct>
|
||||
|
||||
<tp:enum name="Playlist_Ordering" array-name="Playlist_Ordering_List" type="s">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Specifies the ordering of returned playlists.</p>
|
||||
</tp:docstring>
|
||||
<tp:enumvalue suffix="Alphabetical" value="Alphabetical">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Alphabetical ordering by name, ascending.</p>
|
||||
</tp:docstring>
|
||||
</tp:enumvalue>
|
||||
<tp:enumvalue suffix="CreationDate" value="Created">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Ordering by creation date, oldest first.</p>
|
||||
</tp:docstring>
|
||||
</tp:enumvalue>
|
||||
<tp:enumvalue suffix="ModifiedDate" value="Modified">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Ordering by last modified date, oldest first.</p>
|
||||
</tp:docstring>
|
||||
</tp:enumvalue>
|
||||
<tp:enumvalue suffix="LastPlayDate" value="Played">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Ordering by date of last playback, oldest first.</p>
|
||||
</tp:docstring>
|
||||
</tp:enumvalue>
|
||||
<tp:enumvalue suffix="UserDefined" value="User">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A user-defined ordering.</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
Some media players may allow users to order playlists as they
|
||||
wish. This ordering allows playlists to be retreived in that
|
||||
order.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</tp:enumvalue>
|
||||
</tp:enum>
|
||||
|
||||
<method name="ActivatePlaylist" tp:name-for-bindings="Activate_Playlist">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
Starts playing the given playlist.
|
||||
</p>
|
||||
<p>
|
||||
Note that this must be implemented. If the media player does not
|
||||
allow clients to change the playlist, it should not implement this
|
||||
interface at all.
|
||||
</p>
|
||||
<p>
|
||||
It is up to the media player whether this completely replaces the
|
||||
current tracklist, or whether it is merely inserted into the
|
||||
tracklist and the first track starts. For example, if the media
|
||||
player is operating in a "jukebox" mode, it may just append the
|
||||
playlist to the list of upcoming tracks, and skip to the first
|
||||
track in the playlist.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
<arg direction="in" name="PlaylistId" type="o">
|
||||
<tp:docstring>
|
||||
<p>The id of the playlist to activate.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
</method>
|
||||
|
||||
<method name="GetPlaylists" tp:name-for-bindings="Get_Playlists">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Gets a set of playlists.</p>
|
||||
</tp:docstring>
|
||||
<arg direction="in" name="Index" type="u">
|
||||
<tp:docstring>
|
||||
<p>The index of the first playlist to be fetched (according to the ordering).</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<arg direction="in" name="MaxCount" type="u">
|
||||
<tp:docstring>
|
||||
<p>The maximum number of playlists to fetch.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<arg direction="in" name="Order" type="s" tp:type="Playlist_Ordering">
|
||||
<tp:docstring>
|
||||
<p>The ordering that should be used.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<arg direction="in" name="ReverseOrder" type="b">
|
||||
<tp:docstring>
|
||||
<p>Whether the order should be reversed.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<arg direction="out" name="Playlists" type="a(oss)" tp:type="Playlist[]">
|
||||
<tp:docstring>
|
||||
<p>A list of (at most MaxCount) playlists.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
</method>
|
||||
|
||||
<property name="PlaylistCount" type="u" tp:name-for-bindings="Playlist_Count" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
The number of playlists available.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="Orderings" tp:name-for-bindings="Orderings" type="as" tp:type="Playlist_Ordering[]" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
The available orderings. At least one must be offered.
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
Media players may not have access to all the data required for some
|
||||
orderings. For example, creation times are not available on UNIX
|
||||
filesystems (don't let the ctime fool you!). On the other hand,
|
||||
clients should have some way to get the "most recent" playlists.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="ActivePlaylist" type="(b(oss))" tp:name-for-bindings="Active_Playlist" tp:type="Maybe_Playlist" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
The currently-active playlist.
|
||||
</p>
|
||||
<p>
|
||||
If there is no currently-active playlist, the structure's Valid field
|
||||
will be false, and the Playlist details are undefined.
|
||||
</p>
|
||||
<p>
|
||||
Note that this may not have a value even after ActivatePlaylist is
|
||||
called with a valid playlist id as ActivatePlaylist implementations
|
||||
have the option of simply inserting the contents of the playlist into
|
||||
the current tracklist.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<signal name="PlaylistChanged" tp:name-for-bindings="Playlist_Changed">
|
||||
<arg name="Playlist" type="(oss)" tp:type="Playlist">
|
||||
<tp:docstring>
|
||||
The playlist which details have changed.
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Indicates that either the Name or Icon attribute of a
|
||||
playlist has changed.
|
||||
</p>
|
||||
<p>Client implementations should be aware that this signal
|
||||
may not be implemented.
|
||||
</p>
|
||||
<tp:rationale>
|
||||
Without this signal, media players have no way to notify clients
|
||||
of a change in the attributes of a playlist other than the active one
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</signal>
|
||||
|
||||
</interface>
|
||||
</node>
|
||||
<!-- vim:set sw=2 sts=2 et ft=xml: -->
|
||||
|
349
libremsonic/ui/mpris_specs/org.mpris.MediaPlayer2.TrackList.xml
Normal file
349
libremsonic/ui/mpris_specs/org.mpris.MediaPlayer2.TrackList.xml
Normal file
@@ -0,0 +1,349 @@
|
||||
<?xml version="1.0" ?>
|
||||
<node name="/Track_List_Interface" xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0">
|
||||
<interface name="org.mpris.MediaPlayer2.TrackList">
|
||||
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
Provides access to a short list of tracks which were recently played or
|
||||
will be played shortly. This is intended to provide context to the
|
||||
currently-playing track, rather than giving complete access to the
|
||||
media player's playlist.
|
||||
</p>
|
||||
<p>
|
||||
Example use cases are the list of tracks from the same album as the
|
||||
currently playing song or the
|
||||
<a href="http://projects.gnome.org/rhythmbox/">Rhythmbox</a> play queue.
|
||||
</p>
|
||||
<p>
|
||||
Each track in the tracklist has a unique identifier.
|
||||
The intention is that this uniquely identifies the track within
|
||||
the scope of the tracklist. In particular, if a media item
|
||||
(a particular music file, say) occurs twice in the track list, each
|
||||
occurrence should have a different identifier. If a track is removed
|
||||
from the middle of the playlist, it should not affect the track ids
|
||||
of any other tracks in the tracklist.
|
||||
</p>
|
||||
<p>
|
||||
As a result, the traditional track identifiers of URLs and position
|
||||
in the playlist cannot be used. Any scheme which satisfies the
|
||||
uniqueness requirements is valid, as clients should not make any
|
||||
assumptions about the value of the track id beyond the fact
|
||||
that it is a unique identifier.
|
||||
</p>
|
||||
<p>
|
||||
Note that the (memory and processing) burden of implementing the
|
||||
TrackList interface and maintaining unique track ids for the
|
||||
playlist can be mitigated by only exposing a subset of the playlist when
|
||||
it is very long (the 20 or so tracks around the currently playing
|
||||
track, for example). This is a recommended practice as the tracklist
|
||||
interface is not designed to enable browsing through a large list of tracks,
|
||||
but rather to provide clients with context about the currently playing track.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
|
||||
<tp:mapping name="Metadata_Map" array-name="Metadata_Map_List">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A mapping from metadata attribute names to values.</p>
|
||||
<p>
|
||||
The <b>mpris:trackid</b> attribute must always be present, and must be
|
||||
of D-Bus type "o". This contains a D-Bus path that uniquely identifies
|
||||
the track within the scope of the playlist. There may or may not be
|
||||
an actual D-Bus object at that path; this specification says nothing
|
||||
about what interfaces such an object may implement.
|
||||
</p>
|
||||
<p>
|
||||
If the length of the track is known, it should be provided in the
|
||||
metadata property with the "mpris:length" key. The length must be
|
||||
given in microseconds, and be represented as a signed 64-bit integer.
|
||||
</p>
|
||||
<p>
|
||||
If there is an image associated with the track, a URL for it may be
|
||||
provided using the "mpris:artUrl" key. For other metadata, fields
|
||||
defined by the
|
||||
<a href="http://xesam.org/main/XesamOntology">Xesam ontology</a>
|
||||
should be used, prefixed by "xesam:". See the
|
||||
<a href="http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata">metadata page on the freedesktop.org wiki</a>
|
||||
for a list of common fields.
|
||||
</p>
|
||||
<p>
|
||||
Lists of strings should be passed using the array-of-string ("as")
|
||||
D-Bus type. Dates should be passed as strings using the ISO 8601
|
||||
extended format (eg: 2007-04-29T14:35:51). If the timezone is
|
||||
known, RFC 3339's internet profile should be used (eg:
|
||||
2007-04-29T14:35:51+02:00).
|
||||
</p>
|
||||
</tp:docstring>
|
||||
<tp:member type="s" name="Attribute">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
The name of the attribute; see the
|
||||
<a href="http://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata">metadata page</a>
|
||||
for guidelines on names to use.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</tp:member>
|
||||
<tp:member type="v" name="Value">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The value of the attribute, in the most appropriate format.</p>
|
||||
</tp:docstring>
|
||||
</tp:member>
|
||||
</tp:mapping>
|
||||
|
||||
<tp:simple-type name="Uri" type="s">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A unique resource identifier.</p>
|
||||
</tp:docstring>
|
||||
</tp:simple-type>
|
||||
|
||||
<method name="GetTracksMetadata" tp:name-for-bindings="Get_Tracks_Metadata">
|
||||
<arg direction="in" name="TrackIds" type="ao" tp:type="Track_Id[]">
|
||||
<tp:docstring>
|
||||
<p>The list of track ids for which metadata is requested.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<arg direction="out" type="aa{sv}" tp:type="Metadata_Map[]" name="Metadata">
|
||||
<tp:docstring>
|
||||
<p>Metadata of the set of tracks given as input.</p>
|
||||
<p>See the type documentation for more details.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Gets all the metadata available for a set of tracks.</p>
|
||||
<p>
|
||||
Each set of metadata must have a "mpris:trackid" entry at the very least,
|
||||
which contains a string that uniquely identifies this track within
|
||||
the scope of the tracklist.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="AddTrack" tp:name-for-bindings="Add_Track">
|
||||
<arg direction="in" type="s" tp:type="Uri" name="Uri">
|
||||
<tp:docstring>
|
||||
<p>
|
||||
The uri of the item to add. Its uri scheme should be an element of the
|
||||
<strong>org.mpris.MediaPlayer2.SupportedUriSchemes</strong>
|
||||
property and the mime-type should match one of the elements of the
|
||||
<strong>org.mpris.MediaPlayer2.SupportedMimeTypes</strong>
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<arg direction="in" type="o" tp:type="Track_Id" name="AfterTrack">
|
||||
<tp:docstring>
|
||||
<p>
|
||||
The identifier of the track after which
|
||||
the new item should be inserted. The path
|
||||
<literal>/org/mpris/MediaPlayer2/TrackList/NoTrack</literal>
|
||||
indicates that the track should be inserted at the
|
||||
start of the track list.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<arg direction="in" type="b" name="SetAsCurrent">
|
||||
<tp:docstring>
|
||||
<p>
|
||||
Whether the newly inserted track should be considered as
|
||||
the current track. Setting this to true has the same effect as
|
||||
calling GoTo afterwards.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Adds a URI in the TrackList.</p>
|
||||
<p>
|
||||
If the <tp:member-ref>CanEditTracks</tp:member-ref> property is false,
|
||||
this has no effect.
|
||||
</p>
|
||||
<p>
|
||||
Note: Clients should not assume that the track has been added at the
|
||||
time when this method returns. They should wait for a TrackAdded (or
|
||||
TrackListReplaced) signal.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="RemoveTrack" tp:name-for-bindings="Remove__Track">
|
||||
<arg direction="in" type="o" tp:type="Track_Id" name="TrackId">
|
||||
<tp:docstring>
|
||||
<p>Identifier of the track to be removed.</p>
|
||||
<p>
|
||||
<literal>/org/mpris/MediaPlayer2/TrackList/NoTrack</literal>
|
||||
is <em>not</em> a valid value for this argument.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Removes an item from the TrackList.</p>
|
||||
<p>If the track is not part of this tracklist, this has no effect.</p>
|
||||
<p>
|
||||
If the <tp:member-ref>CanEditTracks</tp:member-ref> property is false,
|
||||
this has no effect.
|
||||
</p>
|
||||
<p>
|
||||
Note: Clients should not assume that the track has been removed at the
|
||||
time when this method returns. They should wait for a TrackRemoved (or
|
||||
TrackListReplaced) signal.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="GoTo" tp:name-for-bindings="Go_To">
|
||||
<arg direction="in" type="o" tp:type="Track_Id" name="TrackId">
|
||||
<tp:docstring>
|
||||
<p>Identifier of the track to skip to.</p>
|
||||
<p>
|
||||
<literal>/org/mpris/MediaPlayer2/TrackList/NoTrack</literal>
|
||||
is <em>not</em> a valid value for this argument.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Skip to the specified TrackId.</p>
|
||||
<p>If the track is not part of this tracklist, this has no effect.</p>
|
||||
<p>
|
||||
If this object is not <strong>/org/mpris/MediaPlayer2</strong>,
|
||||
the current TrackList's tracks should be replaced with the contents of
|
||||
this TrackList, and the TrackListReplaced signal should be fired from
|
||||
<strong>/org/mpris/MediaPlayer2</strong>.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<property name="Tracks" type="ao" tp:type="Track_Id[]" tp:name-for-bindings="Tracks" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="invalidates"/>
|
||||
<tp:docstring>
|
||||
<p>
|
||||
An array which contains the identifier of each track
|
||||
in the tracklist, in order.
|
||||
</p>
|
||||
<p>
|
||||
The <literal>org.freedesktop.DBus.Properties.PropertiesChanged</literal>
|
||||
signal is emited every time this property changes, but the signal
|
||||
message does not contain the new value.
|
||||
|
||||
Client implementations should rather rely on the
|
||||
<tp:member-ref>TrackAdded</tp:member-ref>,
|
||||
<tp:member-ref>TrackRemoved</tp:member-ref> and
|
||||
<tp:member-ref>TrackListReplaced</tp:member-ref> signals to keep their
|
||||
representation of the tracklist up to date.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="CanEditTracks" type="b" tp:name-for-bindings="Can_Edit_Tracks" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
If <strong>false</strong>, calling
|
||||
<tp:member-ref>AddTrack</tp:member-ref> or
|
||||
<tp:member-ref>RemoveTrack</tp:member-ref> will have no effect,
|
||||
and may raise a NotSupported error.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<signal name="TrackListReplaced" tp:name-for-bindings="Track_List_Replaced">
|
||||
<arg name="Tracks" type="ao" tp:type="Track_Id[]">
|
||||
<tp:docstring>
|
||||
<p>The new content of the tracklist.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<arg name="CurrentTrack" type="o" tp:type="Track_Id">
|
||||
<tp:docstring>
|
||||
<p>The identifier of the track to be considered as current.</p>
|
||||
<p>
|
||||
<literal>/org/mpris/MediaPlayer2/TrackList/NoTrack</literal>
|
||||
indicates that there is no current track.
|
||||
</p>
|
||||
<p>
|
||||
This should correspond to the <literal>mpris:trackid</literal> field of the
|
||||
Metadata property of the <literal>org.mpris.MediaPlayer2.Player</literal>
|
||||
interface.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Indicates that the entire tracklist has been replaced.</p>
|
||||
<p>
|
||||
It is left up to the implementation to decide when
|
||||
a change to the track list is invasive enough that
|
||||
this signal should be emitted instead of a series of
|
||||
TrackAdded and TrackRemoved signals.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</signal>
|
||||
|
||||
<signal name="TrackAdded" tp:name-for-bindings="Track_Added">
|
||||
<arg type="a{sv}" tp:type="Metadata_Map" name="Metadata">
|
||||
<tp:docstring>
|
||||
<p>The metadata of the newly added item.</p>
|
||||
<p>This must include a mpris:trackid entry.</p>
|
||||
<p>See the type documentation for more details.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<arg type="o" tp:type="Track_Id" name="AfterTrack">
|
||||
<tp:docstring>
|
||||
<p>
|
||||
The identifier of the track after which the new track
|
||||
was inserted. The path
|
||||
<literal>/org/mpris/MediaPlayer2/TrackList/NoTrack</literal>
|
||||
indicates that the track was inserted at the
|
||||
start of the track list.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Indicates that a track has been added to the track list.</p>
|
||||
</tp:docstring>
|
||||
</signal>
|
||||
|
||||
<signal name="TrackRemoved" tp:name-for-bindings="Track_Removed">
|
||||
<arg type="o" tp:type="Track_Id" name="TrackId">
|
||||
<tp:docstring>
|
||||
<p>The identifier of the track being removed.</p>
|
||||
<p>
|
||||
<literal>/org/mpris/MediaPlayer2/TrackList/NoTrack</literal>
|
||||
is <em>not</em> a valid value for this argument.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Indicates that a track has been removed from the track list.</p>
|
||||
</tp:docstring>
|
||||
</signal>
|
||||
|
||||
<signal name="TrackMetadataChanged" tp:name-for-bindings="Track_Metadata_Changed">
|
||||
<arg type="o" tp:type="Track_Id" name="TrackId">
|
||||
<tp:docstring>
|
||||
<p>The id of the track which metadata has changed.</p>
|
||||
<p>If the track id has changed, this will be the old value.</p>
|
||||
<p>
|
||||
<literal>/org/mpris/MediaPlayer2/TrackList/NoTrack</literal>
|
||||
is <em>not</em> a valid value for this argument.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<arg type="a{sv}" tp:type="Metadata_Map" name="Metadata">
|
||||
<tp:docstring>
|
||||
<p>The new track metadata.</p>
|
||||
<p>
|
||||
This must include a mpris:trackid entry. If the track id has
|
||||
changed, this will be the new value.
|
||||
</p>
|
||||
<p>See the type documentation for more details.</p>
|
||||
</tp:docstring>
|
||||
</arg>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
Indicates that the metadata of a track in the tracklist has changed.
|
||||
</p>
|
||||
<p>
|
||||
This may indicate that a track has been replaced, in which case the
|
||||
mpris:trackid metadata entry is different from the TrackId argument.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</signal>
|
||||
|
||||
</interface>
|
||||
</node>
|
||||
<!-- vim:set sw=2 sts=2 et ft=xml: -->
|
198
libremsonic/ui/mpris_specs/org.mpris.MediaPlayer2.xml
Normal file
198
libremsonic/ui/mpris_specs/org.mpris.MediaPlayer2.xml
Normal file
@@ -0,0 +1,198 @@
|
||||
<?xml version="1.0" ?>
|
||||
<node name="/Media_Player" xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0">
|
||||
<interface name="org.mpris.MediaPlayer2">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
|
||||
<method name="Raise" tp:name-for-bindings="Raise">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
Brings the media player's user interface to the front using any
|
||||
appropriate mechanism available.
|
||||
</p>
|
||||
<p>
|
||||
The media player may be unable to control how its user interface
|
||||
is displayed, or it may not have a graphical user interface at all.
|
||||
In this case, the <tp:member-ref>CanRaise</tp:member-ref> property is
|
||||
<strong>false</strong> and this method does nothing.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<method name="Quit" tp:name-for-bindings="Quit">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Causes the media player to stop running.</p>
|
||||
<p>
|
||||
The media player may refuse to allow clients to shut it down.
|
||||
In this case, the <tp:member-ref>CanQuit</tp:member-ref> property is
|
||||
<strong>false</strong> and this method does nothing.
|
||||
</p>
|
||||
<p>
|
||||
Note: Media players which can be D-Bus activated, or for which there is
|
||||
no sensibly easy way to terminate a running instance (via the main
|
||||
interface or a notification area icon for example) should allow clients
|
||||
to use this method. Otherwise, it should not be needed.
|
||||
</p>
|
||||
<p>If the media player does not have a UI, this should be implemented.</p>
|
||||
</tp:docstring>
|
||||
</method>
|
||||
|
||||
<property name="CanQuit" type="b" tp:name-for-bindings="Can_Quit" access="read">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
If <strong>false</strong>, calling
|
||||
<tp:member-ref>Quit</tp:member-ref> will have no effect, and may
|
||||
raise a NotSupported error. If <strong>true</strong>, calling
|
||||
<tp:member-ref>Quit</tp:member-ref> will cause the media application
|
||||
to attempt to quit (although it may still be prevented from quitting
|
||||
by the user, for example).
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="Fullscreen" type="b" tp:name-for-bindings="Fullscreen" access="readwrite">
|
||||
<tp:added version="2.2" />
|
||||
<annotation name="org.mpris.MediaPlayer2.property.optional" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>Whether the media player is occupying the fullscreen.</p>
|
||||
<p>
|
||||
This is typically used for videos. A value of <strong>true</strong>
|
||||
indicates that the media player is taking up the full screen.
|
||||
</p>
|
||||
<p>
|
||||
Media centre software may well have this value fixed to <strong>true</strong>
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanSetFullscreen</tp:member-ref> is <strong>true</strong>,
|
||||
clients may set this property to <strong>true</strong> to tell the media player
|
||||
to enter fullscreen mode, or to <strong>false</strong> to return to windowed
|
||||
mode.
|
||||
</p>
|
||||
<p>
|
||||
If <tp:member-ref>CanSetFullscreen</tp:member-ref> is <strong>false</strong>,
|
||||
then attempting to set this property should have no effect, and may raise
|
||||
an error. However, even if it is <strong>true</strong>, the media player
|
||||
may still be unable to fulfil the request, in which case attempting to set
|
||||
this property will have no effect (but should not raise an error).
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
This allows remote control interfaces, such as LIRC or mobile devices like
|
||||
phones, to control whether a video is shown in fullscreen.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="CanSetFullscreen" type="b" tp:name-for-bindings="Can_Set_Fullscreen" access="read">
|
||||
<tp:added version="2.2" />
|
||||
<annotation name="org.mpris.MediaPlayer2.property.optional" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
If <strong>false</strong>, attempting to set
|
||||
<tp:member-ref>Fullscreen</tp:member-ref> will have no effect, and may
|
||||
raise an error. If <strong>true</strong>, attempting to set
|
||||
<tp:member-ref>Fullscreen</tp:member-ref> will not raise an error, and (if it
|
||||
is different from the current value) will cause the media player to attempt to
|
||||
enter or exit fullscreen mode.
|
||||
</p>
|
||||
<p>
|
||||
Note that the media player may be unable to fulfil the request.
|
||||
In this case, the value will not change. If the media player knows in
|
||||
advance that it will not be able to fulfil the request, however, this
|
||||
property should be <strong>false</strong>.
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
This allows clients to choose whether to display controls for entering
|
||||
or exiting fullscreen mode.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="CanRaise" type="b" tp:name-for-bindings="Can_Raise" access="read">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
If <strong>false</strong>, calling
|
||||
<tp:member-ref>Raise</tp:member-ref> will have no effect, and may
|
||||
raise a NotSupported error. If <strong>true</strong>, calling
|
||||
<tp:member-ref>Raise</tp:member-ref> will cause the media application
|
||||
to attempt to bring its user interface to the front, although it may
|
||||
be prevented from doing so (by the window manager, for example).
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="HasTrackList" type="b" tp:name-for-bindings="Has_TrackList" access="read">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
Indicates whether the <strong>/org/mpris/MediaPlayer2</strong>
|
||||
object implements the <strong>org.mpris.MediaPlayer2.TrackList</strong>
|
||||
interface.
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="Identity" type="s" tp:name-for-bindings="Identity" access="read">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>A friendly name to identify the media player to users.</p>
|
||||
<p>This should usually match the name found in .desktop files</p>
|
||||
<p>(eg: "VLC media player").</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="DesktopEntry" type="s" tp:name-for-bindings="Desktop_Entry" access="read">
|
||||
<annotation name="org.mpris.MediaPlayer2.property.optional" value="true"/>
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>The basename of an installed .desktop file which complies with the <a href="http://standards.freedesktop.org/desktop-entry-spec/latest/">Desktop entry specification</a>,
|
||||
with the ".desktop" extension stripped.</p>
|
||||
<p>
|
||||
Example: The desktop entry file is "/usr/share/applications/vlc.desktop",
|
||||
and this property contains "vlc"
|
||||
</p>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="SupportedUriSchemes" type="as" tp:name-for-bindings="Supported_Uri_Schemes" access="read">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
The URI schemes supported by the media player.
|
||||
</p>
|
||||
<p>
|
||||
This can be viewed as protocols supported by the player in almost
|
||||
all cases. Almost every media player will include support for the
|
||||
"file" scheme. Other common schemes are "http" and "rtsp".
|
||||
</p>
|
||||
<p>
|
||||
Note that URI schemes should be lower-case.
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
This is important for clients to know when using the editing
|
||||
capabilities of the Playlist interface, for example.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
<property name="SupportedMimeTypes" type="as" tp:name-for-bindings="Supported_Mime_Types" access="read">
|
||||
<tp:docstring xmlns="http://www.w3.org/1999/xhtml">
|
||||
<p>
|
||||
The mime-types supported by the media player.
|
||||
</p>
|
||||
<p>
|
||||
Mime-types should be in the standard format (eg: audio/mpeg or
|
||||
application/ogg).
|
||||
</p>
|
||||
<tp:rationale>
|
||||
<p>
|
||||
This is important for clients to know when using the editing
|
||||
capabilities of the Playlist interface, for example.
|
||||
</p>
|
||||
</tp:rationale>
|
||||
</tp:docstring>
|
||||
</property>
|
||||
|
||||
</interface>
|
||||
</node>
|
||||
<!-- vim:set sw=2 sts=2 et ft=xml: -->
|
@@ -10,8 +10,6 @@ from libremsonic.ui import util
|
||||
from libremsonic.ui.common import IconButton, SpinnerImage
|
||||
from libremsonic.ui.common.players import ChromecastPlayer
|
||||
|
||||
from libremsonic.server.api_objects import Child
|
||||
|
||||
|
||||
class PlayerControls(Gtk.ActionBar):
|
||||
"""
|
||||
@@ -72,8 +70,8 @@ class PlayerControls(Gtk.ActionBar):
|
||||
icon = 'pause' if state.playing else 'start'
|
||||
self.play_button.set_icon(f"media-playback-{icon}-symbolic")
|
||||
|
||||
has_current_song = (hasattr(state, 'current_song')
|
||||
and state.current_song is not None)
|
||||
has_current_song = (
|
||||
hasattr(state, 'current_song') and state.current_song is not None)
|
||||
has_next_song = False
|
||||
if state.repeat_type in (RepeatType.REPEAT_QUEUE,
|
||||
RepeatType.REPEAT_SONG):
|
||||
@@ -92,8 +90,9 @@ class PlayerControls(Gtk.ActionBar):
|
||||
# TODO: it's not correct to use symboloc vs. not symbolic icons for
|
||||
# lighter/darker versions of the icon. Fix this by using FG color I
|
||||
# think? But then we have to deal with styling, which sucks.
|
||||
self.shuffle_button.set_icon('media-playlist-shuffle' +
|
||||
('-symbolic' if state.shuffle_on else ''))
|
||||
self.shuffle_button.set_icon(
|
||||
'media-playlist-shuffle'
|
||||
+ ('-symbolic' if state.shuffle_on else ''))
|
||||
|
||||
self.song_scrubber.set_sensitive(has_current_song)
|
||||
self.prev_button.set_sensitive(has_current_song)
|
||||
@@ -160,7 +159,7 @@ class PlayerControls(Gtk.ActionBar):
|
||||
self.album_art.set_loading(False)
|
||||
|
||||
def update_scrubber(self, current, duration):
|
||||
if current is None and duration is None:
|
||||
if current is None or duration is None:
|
||||
self.song_duration_label.set_text('-:--')
|
||||
self.song_progress_label.set_text('-:--')
|
||||
self.song_scrubber.set_value(0)
|
||||
@@ -277,10 +276,10 @@ class PlayerControls(Gtk.ActionBar):
|
||||
orientation=Gtk.Orientation.HORIZONTAL, min=0, max=100, step=5)
|
||||
self.song_scrubber.set_name('song-scrubber')
|
||||
self.song_scrubber.set_draw_value(False)
|
||||
self.song_scrubber.connect('button-press-event',
|
||||
self.on_scrub_state_change)
|
||||
self.song_scrubber.connect('button-release-event',
|
||||
self.on_scrub_state_change)
|
||||
self.song_scrubber.connect(
|
||||
'button-press-event', self.on_scrub_state_change)
|
||||
self.song_scrubber.connect(
|
||||
'button-release-event', self.on_scrub_state_change)
|
||||
scrubber_box.pack_start(self.song_scrubber, True, True, 0)
|
||||
|
||||
self.song_duration_label = Gtk.Label(label='-:--')
|
||||
@@ -297,22 +296,25 @@ class PlayerControls(Gtk.ActionBar):
|
||||
buttons.pack_start(self.repeat_button, False, False, 5)
|
||||
|
||||
# Previous button
|
||||
self.prev_button = IconButton('media-skip-backward-symbolic',
|
||||
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||
self.prev_button = IconButton(
|
||||
'media-skip-backward-symbolic',
|
||||
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||
self.prev_button.set_action_name('app.prev-track')
|
||||
buttons.pack_start(self.prev_button, False, False, 5)
|
||||
|
||||
# Play button
|
||||
self.play_button = IconButton('media-playback-start-symbolic',
|
||||
relief=True,
|
||||
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||
self.play_button = IconButton(
|
||||
'media-playback-start-symbolic',
|
||||
relief=True,
|
||||
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||
self.play_button.set_name('play-button')
|
||||
self.play_button.set_action_name('app.play-pause')
|
||||
buttons.pack_start(self.play_button, False, False, 0)
|
||||
|
||||
# Next button
|
||||
self.next_button = IconButton('media-skip-forward-symbolic',
|
||||
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||
self.next_button = IconButton(
|
||||
'media-skip-forward-symbolic',
|
||||
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||
self.next_button.set_action_name('app.next-track')
|
||||
buttons.pack_start(self.next_button, False, False, 5)
|
||||
|
||||
@@ -333,8 +335,8 @@ class PlayerControls(Gtk.ActionBar):
|
||||
|
||||
# Device button (for chromecast)
|
||||
# TODO need icon
|
||||
device_button = IconButton('view-list-symbolic',
|
||||
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||
device_button = IconButton(
|
||||
'view-list-symbolic', icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||
device_button.connect('clicked', self.on_device_click)
|
||||
box.pack_start(device_button, False, True, 5)
|
||||
|
||||
@@ -380,8 +382,8 @@ class PlayerControls(Gtk.ActionBar):
|
||||
self.device_popover.add(device_popover_box)
|
||||
|
||||
# Play Queue button
|
||||
play_queue_button = IconButton('view-list-symbolic',
|
||||
icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||
play_queue_button = IconButton(
|
||||
'view-list-symbolic', icon_size=Gtk.IconSize.LARGE_TOOLBAR)
|
||||
play_queue_button.connect('clicked', self.on_play_queue_click)
|
||||
box.pack_start(play_queue_button, False, True, 5)
|
||||
|
||||
|
@@ -103,15 +103,15 @@ class PlaylistList(Gtk.Box):
|
||||
activatable=False,
|
||||
selectable=False,
|
||||
)
|
||||
loading_spinner = Gtk.Spinner(name='playlist-list-spinner',
|
||||
active=True)
|
||||
loading_spinner = Gtk.Spinner(
|
||||
name='playlist-list-spinner', active=True)
|
||||
self.loading_indicator.add(loading_spinner)
|
||||
loading_new_playlist.add(self.loading_indicator)
|
||||
|
||||
self.new_playlist_row = Gtk.ListBoxRow(activatable=False,
|
||||
selectable=False)
|
||||
new_playlist_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
|
||||
visible=False)
|
||||
self.new_playlist_row = Gtk.ListBoxRow(
|
||||
activatable=False, selectable=False)
|
||||
new_playlist_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, visible=False)
|
||||
|
||||
self.new_playlist_entry = Gtk.Entry(
|
||||
name='playlist-list-new-playlist-entry')
|
||||
@@ -277,23 +277,23 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
view_refresh_button = IconButton('view-refresh-symbolic')
|
||||
view_refresh_button.connect('clicked', self.on_view_refresh_click)
|
||||
self.playlist_action_buttons.pack_end(view_refresh_button, False,
|
||||
False, 5)
|
||||
self.playlist_action_buttons.pack_end(
|
||||
view_refresh_button, False, False, 5)
|
||||
|
||||
playlist_edit_button = IconButton('document-edit-symbolic')
|
||||
playlist_edit_button.connect('clicked',
|
||||
self.on_playlist_edit_button_click)
|
||||
self.playlist_action_buttons.pack_end(playlist_edit_button, False,
|
||||
False, 5)
|
||||
playlist_edit_button.connect(
|
||||
'clicked', self.on_playlist_edit_button_click)
|
||||
self.playlist_action_buttons.pack_end(
|
||||
playlist_edit_button, False, False, 5)
|
||||
|
||||
download_all_button = IconButton('folder-download-symbolic')
|
||||
download_all_button.connect(
|
||||
'clicked', self.on_playlist_list_download_all_button_click)
|
||||
self.playlist_action_buttons.pack_end(download_all_button, False,
|
||||
False, 5)
|
||||
self.playlist_action_buttons.pack_end(
|
||||
download_all_button, False, False, 5)
|
||||
|
||||
playlist_details_box.pack_start(self.playlist_action_buttons, False,
|
||||
False, 5)
|
||||
playlist_details_box.pack_start(
|
||||
self.playlist_action_buttons, False, False, 5)
|
||||
|
||||
playlist_details_box.pack_start(Gtk.Box(), True, False, 0)
|
||||
|
||||
@@ -407,15 +407,15 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
create_column('DURATION', 4, align=1, width=40))
|
||||
|
||||
self.playlist_songs.connect('row-activated', self.on_song_activated)
|
||||
self.playlist_songs.connect('button-press-event',
|
||||
self.on_song_button_press)
|
||||
self.playlist_songs.connect(
|
||||
'button-press-event', self.on_song_button_press)
|
||||
|
||||
# Set up drag-and-drop on the song list for editing the order of the
|
||||
# playlist.
|
||||
self.playlist_song_store.connect('row-inserted',
|
||||
self.playlist_model_row_move)
|
||||
self.playlist_song_store.connect('row-deleted',
|
||||
self.playlist_model_row_move)
|
||||
self.playlist_song_store.connect(
|
||||
'row-inserted', self.playlist_model_row_move)
|
||||
self.playlist_song_store.connect(
|
||||
'row-deleted', self.playlist_model_row_move)
|
||||
|
||||
playlist_view_scroll_window.add(self.playlist_songs)
|
||||
|
||||
@@ -481,14 +481,17 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
# update the list.
|
||||
self.editing_playlist_song_list = True
|
||||
|
||||
new_store = [[
|
||||
util.get_cached_status_icon(CacheManager.get_cached_status(song)),
|
||||
song.title,
|
||||
song.album,
|
||||
song.artist,
|
||||
util.format_song_duration(song.duration),
|
||||
song.id,
|
||||
] for song in (playlist.entry or [])]
|
||||
new_store = [
|
||||
[
|
||||
util.get_cached_status_icon(
|
||||
CacheManager.get_cached_status(song)),
|
||||
song.title,
|
||||
song.album,
|
||||
song.artist,
|
||||
util.format_song_duration(song.duration),
|
||||
song.id,
|
||||
] for song in (playlist.entry or [])
|
||||
]
|
||||
|
||||
util.diff_song_store(self.playlist_song_store, new_store)
|
||||
|
||||
@@ -564,9 +567,15 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
|
||||
def on_play_all_clicked(self, btn):
|
||||
song_id = self.playlist_song_store[0][-1]
|
||||
self.emit('song-clicked', song_id,
|
||||
[m[-1] for m in self.playlist_song_store],
|
||||
{'force_shuffle_state': False})
|
||||
self.emit(
|
||||
'song-clicked',
|
||||
song_id,
|
||||
[m[-1] for m in self.playlist_song_store],
|
||||
{
|
||||
'force_shuffle_state': False,
|
||||
'active_playlist_id': self.playlist_id,
|
||||
},
|
||||
)
|
||||
|
||||
def on_shuffle_all_button(self, btn):
|
||||
rand_idx = randint(0, len(self.playlist_song_store) - 1)
|
||||
@@ -574,7 +583,10 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
'song-clicked',
|
||||
self.playlist_song_store[rand_idx][-1],
|
||||
[m[-1] for m in self.playlist_song_store],
|
||||
{'force_shuffle_state': True},
|
||||
{
|
||||
'force_shuffle_state': True,
|
||||
'active_playlist_id': self.playlist_id,
|
||||
},
|
||||
)
|
||||
|
||||
def on_song_activated(self, treeview, idx, column):
|
||||
@@ -583,7 +595,9 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
'song-clicked',
|
||||
self.playlist_song_store[idx][-1],
|
||||
[m[-1] for m in self.playlist_song_store],
|
||||
{},
|
||||
{
|
||||
'active_playlist_id': self.playlist_id,
|
||||
},
|
||||
)
|
||||
|
||||
def on_song_button_press(self, tree, event):
|
||||
@@ -619,8 +633,9 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
)
|
||||
self.update_playlist_view(self.playlist_id, force=True)
|
||||
|
||||
remove_text = ('Remove ' + util.pluralize('song', len(song_ids))
|
||||
+ ' from playlist')
|
||||
remove_text = (
|
||||
'Remove ' + util.pluralize('song', len(song_ids))
|
||||
+ ' from playlist')
|
||||
util.show_song_popover(
|
||||
song_ids,
|
||||
event.x,
|
||||
@@ -667,8 +682,9 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
song_id_to_add=[s[-1] for s in self.playlist_song_store],
|
||||
)
|
||||
|
||||
update_playlist_future.add_done_callback(lambda f: GLib.idle_add(
|
||||
lambda: self.update_playlist_view(playlist.id, force=True)))
|
||||
update_playlist_future.add_done_callback(
|
||||
lambda f: GLib.idle_add(
|
||||
lambda: self.update_playlist_view(playlist.id, force=True)))
|
||||
|
||||
def format_stats(self, playlist):
|
||||
created_date = playlist.created.strftime('%B %d, %Y')
|
||||
@@ -678,8 +694,9 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
||||
f"{'Not v' if not playlist.public else 'V'}isible to others",
|
||||
),
|
||||
util.dot_join(
|
||||
'{} {}'.format(playlist.songCount,
|
||||
util.pluralize("song", playlist.songCount)),
|
||||
'{} {}'.format(
|
||||
playlist.songCount,
|
||||
util.pluralize("song", playlist.songCount)),
|
||||
util.format_sequence_duration(playlist.duration),
|
||||
),
|
||||
]
|
||||
|
@@ -34,14 +34,14 @@ def format_sequence_duration(duration_secs) -> str:
|
||||
format_components.append(hrs)
|
||||
|
||||
if duration_mins > 0:
|
||||
mins = '{} {}'.format(duration_mins, pluralize('minute',
|
||||
duration_mins))
|
||||
mins = '{} {}'.format(
|
||||
duration_mins, pluralize('minute', duration_mins))
|
||||
format_components.append(mins)
|
||||
|
||||
# Show seconds if there are no hours.
|
||||
if duration_hrs == 0:
|
||||
secs = '{} {}'.format(duration_secs, pluralize('second',
|
||||
duration_secs))
|
||||
secs = '{} {}'.format(
|
||||
duration_secs, pluralize('second', duration_secs))
|
||||
format_components.append(secs)
|
||||
|
||||
return ', '.join(format_components)
|
||||
|
@@ -8,3 +8,4 @@ split_before_arithmetic_operator = true
|
||||
split_before_dot = true
|
||||
split_before_logical_operator = true
|
||||
split_complex_comprehension = true
|
||||
split_before_first_argument = true
|
||||
|
12
setup.py
12
setup.py
@@ -29,7 +29,7 @@ setup(
|
||||
# 3 - Alpha
|
||||
# 4 - Beta
|
||||
# 5 - Production/Stable
|
||||
'Development Status :: 3 - ALpha',
|
||||
'Development Status :: 3 - Alpha',
|
||||
|
||||
# Indicate who your project is intended for
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
@@ -43,7 +43,15 @@ setup(
|
||||
],
|
||||
keywords='airsonic subsonic libresonic music',
|
||||
packages=find_packages(exclude=['tests']),
|
||||
package_data={'libremsonic': ['ui/app_styles.css']},
|
||||
package_data={
|
||||
'libremsonic': [
|
||||
'ui/app_styles.css',
|
||||
'ui/mpris_specs/org.mpris.MediaPlayer2.xml',
|
||||
'ui/mpris_specs/org.mpris.MediaPlayer2.Player.xml',
|
||||
'ui/mpris_specs/org.mpris.MediaPlayer2.Playlists.xml',
|
||||
'ui/mpris_specs/org.mpris.MediaPlayer2.Tracklist.xml',
|
||||
]
|
||||
},
|
||||
install_requires=[
|
||||
'bottle',
|
||||
'deepdiff',
|
||||
|
Reference in New Issue
Block a user