diff --git a/CMakeLists.txt b/CMakeLists.txt index 25a2be4..1f4966d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ set(CMAKE_CXX_STANDARD 17) # Options option(PLATFORM_SAILFISHOS "Build SailfishOS version of application" OFF) option(PLATFORM_QTQUICK "Build QtQuick version of application" ON) +option(FREEDESKTOP_INTEGRATION "Integration with various FreeDesktop.org standards, such as MPRIS" ON) option(BUILD_PRECOMPILED_HEADERS "Build with precompiled headers for faster compile times when doing a full rebuild, at the cost of slower incremental builds whenever a header file is changed" OFF) if (NOT SAILFIN_VERSION) diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index a45f0d1..41316fc 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -1,5 +1,10 @@ project(jellyfin-qt VERSION 0.1.0) find_package(Qt5 5.6 COMPONENTS Multimedia Network Qml WebSockets REQUIRED) + +if (FREEDESKTOP_INTEGRATION) +find_package(Qt5 5.6 COMPONENTS DBus REQUIRED) +endif() + include(GNUInstallDirs) include(GeneratedSources.cmake) @@ -52,6 +57,7 @@ set(JellyfinQt_HEADERS include/JellyfinQt/viewmodel/modelstatus.h include/JellyfinQt/viewmodel/propertyhelper.h include/JellyfinQt/viewmodel/playbackmanager.h + include/JellyfinQt/viewmodel/platformmediacontrol.h include/JellyfinQt/viewmodel/playlist.h include/JellyfinQt/viewmodel/userdata.h include/JellyfinQt/viewmodel/usermodel.h @@ -64,6 +70,19 @@ set(JellyfinQt_HEADERS include/JellyfinQt/jsonhelper.h include/JellyfinQt/serverdiscoverymodel.h include/JellyfinQt/websocket.h) + +if (FREEDESKTOP_INTEGRATION) + list(APPEND JellyfinQt_SOURCES + src/platform/freedesktop/mediaplayer2.cpp + src/platform/freedesktop/mediaplayer2player.cpp + src/viewmodel/platformmediacontrol_freedesktop.cpp) + list(APPEND JellyfinQt_HEADERS + include/JellyfinQt/platform/freedesktop/mediaplayer2.h + include/JellyfinQt/platform/freedesktop/mediaplayer2player.h) +else() + list(APPEND JellyfinQt_SOURCES + src/viewmodel/platformmediacontrol_stub.cpp) +endif() list(APPEND JellyfinQt_HEADERS ${openapi_HEADERS}) @@ -83,6 +102,10 @@ endif() target_include_directories(JellyfinQt PUBLIC "include") target_link_libraries(JellyfinQt PUBLIC Qt5::Core Qt5::Multimedia Qt5::Network Qt5::Qml Qt5::WebSockets) + +if (FREEDESKTOP_INTEGRATION) + target_link_libraries(JellyfinQt PUBLIC Qt5::DBus) +endif() set_target_properties(JellyfinQt PROPERTIES CXX_VISIBILITY_PRESET default) install(TARGETS JellyfinQt LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" diff --git a/core/dbus/org.mpris.MediaPlayer2.Player.xml b/core/dbus/org.mpris.MediaPlayer2.Player.xml new file mode 100644 index 0000000..9cfc223 --- /dev/null +++ b/core/dbus/org.mpris.MediaPlayer2.Player.xml @@ -0,0 +1,673 @@ + + + + + +

+ This interface implements the methods for querying and providing basic + control over what is currently playing. +

+
+ + + + +

A track is currently playing.

+
+
+ + +

A track is currently paused.

+
+
+ + +

There is no track currently playing.

+
+
+ +

A playback state.

+
+
+ + + + +

The playback will stop when there are no more tracks to play

+
+
+ + +

The current track will start again from the begining once it has finished playing

+
+
+ + +

The playback loops through a list of tracks

+
+
+ +

A repeat / loop status

+
+
+ + + +

Unique track identifier.

+

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

+

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

+

+ Media players may not use any paths starting with + /org/mpris unless explicitly allowed by this specification. + Such paths are intended to have special meaning, such as + /org/mpris/MediaPlayer2/TrackList/NoTrack + to indicate "no track". +

+ +

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

+
+
+
+ + + +

A playback rate

+

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

+
+
+ + + +

Audio volume level

+
    +
  • 0.0 means mute.
  • +
  • 1.0 is a sensible maximum volume level (ex: 0dB).
  • +
+

+ Note that the volume may be higher than 1.0, although generally + clients should not attempt to set it above 1.0. +

+
+
+ + + +

Time in microseconds.

+
+
+ + + +

Skips to the next track in the tracklist.

+

+ If there is no next track (and endless playback and track + repeat are both off), stop playback. +

+

If playback is paused or stopped, it remains that way.

+

+ If CanGoNext is + false, attempting to call this method should have + no effect. +

+
+
+ + + +

Skips to the previous track in the tracklist.

+

+ If there is no previous track (and endless playback and track + repeat are both off), stop playback. +

+

If playback is paused or stopped, it remains that way.

+

+ If CanGoPrevious is + false, attempting to call this method should have + no effect. +

+
+
+ + + +

Pauses playback.

+

If playback is already paused, this has no effect.

+

+ Calling Play after this should cause playback to start again + from the same position. +

+

+ If CanPause is + false, attempting to call this method should have + no effect. +

+
+
+ + + +

Pauses playback.

+

If playback is already paused, resumes playback.

+

If playback is stopped, starts playback.

+

+ If CanPause is + false, attempting to call this method should have + no effect and raise an error. +

+
+
+ + + +

Stops playback.

+

If playback is already stopped, this has no effect.

+

+ Calling Play after this should cause playback to + start again from the beginning of the track. +

+

+ If CanControl is + false, attempting to call this method should have + no effect and raise an error. +

+
+
+ + + +

Starts or resumes playback.

+

If already playing, this has no effect.

+

If paused, playback resumes from the current position.

+

If there is no track to play, this has no effect.

+

+ If CanPlay is + false, attempting to call this method should have + no effect. +

+
+
+ + + + +

The number of microseconds to seek forward.

+
+
+ +

+ Seeks forward in the current track by the specified number + of microseconds. +

+

+ A negative value seeks back. If this would mean seeking + back further than the start of the track, the position + is set to 0. +

+

+ If the value passed in would mean seeking beyond the end + of the track, acts like a call to Next. +

+

+ If the CanSeek property is false, + this has no effect. +

+
+
+ + + + +

The currently playing track's identifier.

+

+ If this does not match the id of the currently-playing track, + the call is ignored as "stale". +

+

+ /org/mpris/MediaPlayer2/TrackList/NoTrack + is not a valid value for this argument. +

+
+
+ + +

Track position in microseconds.

+

This must be between 0 and <track_length>.

+
+
+ +

Sets the current track position in microseconds.

+

If the Position argument is less than 0, do nothing.

+

+ If the Position argument is greater than the track length, + do nothing. +

+

+ If the CanSeek property is false, + this has no effect. +

+ +

+ The reason for having this method, rather than making + Position 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. +

+
+
+
+ + + + +

+ Uri of the track to load. Its uri scheme should be an element of the + org.mpris.MediaPlayer2.SupportedUriSchemes + property and the mime-type should match one of the elements of the + org.mpris.MediaPlayer2.SupportedMimeTypes. +

+
+
+ +

Opens the Uri given as an argument

+

If the playback is stopped, starts playing

+

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

+

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 + Metadata property changes. +

+

+ If the media player implements the TrackList interface, then the + opened track should be made part of the tracklist, the + org.mpris.MediaPlayer2.TrackList.TrackAdded or + org.mpris.MediaPlayer2.TrackList.TrackListReplaced + signal should be fired, as well as the + org.freedesktop.DBus.Properties.PropertiesChanged + signal on the tracklist interface. +

+
+
+ + + + +

The current playback status.

+

+ May be "Playing", "Paused" or "Stopped". +

+
+
+ + + + + +

The current loop / repeat status

+

May be: +

    +
  • "None" if the playback will stop when there are no more tracks to play
  • +
  • "Track" if the current track will start again from the begining once it has finished playing
  • +
  • "Playlist" if the playback loops through a list of tracks
  • +
+

+

+ If CanControl is + false, attempting to set this property should have + no effect and raise an error. +

+
+
+ + + + +

The current playback rate.

+

+ The value must fall in the range described by + MinimumRate and + MaximumRate, and must not be 0.0. If + playback is paused, the PlaybackStatus + 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 Pause was called. +

+

+ 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 MinimumRate and + MaximumRate properties must also be + set to 1.0. +

+

+ 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). +

+ +

+ This allows clients to display (reasonably) accurate progress bars + without having to regularly query the media player for the current + position. +

+
+
+
+ + + + + +

+ A value of false indicates that playback is + progressing linearly through a playlist, while true + means playback is progressing through a playlist in some other order. +

+

+ If CanControl is + false, attempting to set this property should have + no effect and raise an error. +

+
+
+ + + + + +

The metadata of the current element.

+

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

+

+ See the type documentation for more details. +

+
+
+ + + + +

The volume level.

+

+ When setting, if a negative value is passed, the volume + should be set to 0.0. +

+

+ If CanControl is + false, attempting to set this property should have + no effect and raise an error. +

+
+
+ + + + +

+ The current track position in microseconds, between 0 and + the 'mpris:length' metadata entry (see Metadata). +

+

+ 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 + CanSeek property is false, and + setting this property has no effect and can raise an error. +

+

+ If the playback progresses in a way that is inconstistant with the + Rate property, the + Seeked signal is emited. +

+
+
+ + + + +

+ The minimum value which the Rate + property can take. + Clients should not attempt to set the + Rate property below this value. +

+

+ Note that even if this value is 0.0 or negative, clients should + not attempt to set the Rate property + to 0.0. +

+

This value should always be 1.0 or less.

+
+
+ + + + +

+ The maximum value which the Rate + property can take. + Clients should not attempt to set the + Rate property above this value. +

+

+ This value should always be 1.0 or greater. +

+
+
+ + + + +

+ Whether the client can call the Next + method on this interface and expect the current track to change. +

+

+ If it is unknown whether a call to Next will + be successful (for example, when streaming tracks), this property should + be set to true. +

+

+ If CanControl is + false, this property should also be + false. +

+ +

+ Even when playback can generally be controlled, there may not + always be a next track to move to. +

+
+
+
+ + + + +

+ Whether the client can call the + Previous method on this interface and + expect the current track to change. +

+

+ If it is unknown whether a call to Previous + will be successful (for example, when streaming tracks), this property + should be set to true. +

+

+ If CanControl is + false, this property should also be + false. +

+ +

+ Even when playback can generally be controlled, there may not + always be a next previous to move to. +

+
+ +
+
+ + + + +

Whether playback can be started using + Play or + PlayPause. +

+

+ 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 + CanControl is true), + this should be true. +

+

+ If CanControl is + false, this property should also be + false. +

+ +

+ 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". +

+
+
+
+ + + + +

Whether playback can be paused using + Pause or + PlayPause. +

+

+ 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 + CanControl is true), + this should be true. +

+

+ If CanControl is + false, this property should also be + false. +

+ +

+ Not all media is pausable: it may not be possible to pause some + streamed media, for example. +

+
+
+
+ + + + +

+ Whether the client can control the playback position using + Seek and + SetPosition. This may be different for + different tracks. +

+

+ If CanControl is + false, this property should also be + false. +

+ +

+ Not all media is seekable: it may not be possible to seek when + playing some streamed media, for example. +

+
+
+
+ + + + +

Whether the media player may be controlled over this interface.

+

+ This property is not expected to change, as it describes an intrinsic + capability of the implementation. +

+

+ If this is false, 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 + false. +

+ +

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

+
+
+
+ + + + +

The new position, in microseconds.

+
+
+ +

+ Indicates that the track position has changed in a way that is + inconsistant with the current playing state. +

+

When this signal is not received, clients should assume that:

+
    +
  • + When playing, the position progresses according to the rate property. +
  • +
  • When paused, it remains constant.
  • +
+

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

+
+
+ +
+
+ diff --git a/core/dbus/org.mpris.MediaPlayer2.Playlists.xml b/core/dbus/org.mpris.MediaPlayer2.Playlists.xml new file mode 100644 index 0000000..e285ef2 --- /dev/null +++ b/core/dbus/org.mpris.MediaPlayer2.Playlists.xml @@ -0,0 +1,250 @@ + + + + + +

Provides access to the media player's playlists.

+

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

+
+ + + +

Unique playlist identifier.

+ +

+ Multiple playlists may have the same name. +

+

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

+
+
+
+ + + +

A URI.

+
+
+ + + +

A data structure describing a playlist.

+
+ + +

A unique identifier for the playlist.

+

This should remain the same if the playlist is renamed.

+
+
+ + +

The name of the playlist, typically given by the user.

+
+
+ + +

The URI of an (optional) icon.

+
+
+
+ + + +

A data structure describing a playlist, or nothing.

+ +

+ D-Bus does not (at the time of writing) support a MAYBE type, + so we are forced to invent our own. +

+
+
+ + +

Whether this structure refers to a valid playlist.

+
+
+ + +

The playlist, providing Valid is true, otherwise undefined.

+

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

+
+
+
+ + + +

Specifies the ordering of returned playlists.

+
+ + +

Alphabetical ordering by name, ascending.

+
+
+ + +

Ordering by creation date, oldest first.

+
+
+ + +

Ordering by last modified date, oldest first.

+
+
+ + +

Ordering by date of last playback, oldest first.

+
+
+ + +

A user-defined ordering.

+ +

+ Some media players may allow users to order playlists as they + wish. This ordering allows playlists to be retreived in that + order. +

+
+
+
+
+ + + +

+ Starts playing the given playlist. +

+

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

+

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

+
+ + +

The id of the playlist to activate.

+
+
+
+ + + +

Gets a set of playlists.

+
+ + +

The index of the first playlist to be fetched (according to the ordering).

+
+
+ + +

The maximum number of playlists to fetch.

+
+
+ + +

The ordering that should be used.

+
+
+ + +

Whether the order should be reversed.

+
+
+ + +

A list of (at most MaxCount) playlists.

+
+
+
+ + + + +

+ The number of playlists available. +

+
+
+ + + + +

+ The available orderings. At least one must be offered. +

+ +

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

+
+
+
+ + + + +

+ The currently-active playlist. +

+

+ If there is no currently-active playlist, the structure's Valid field + will be false, and the Playlist details are undefined. +

+

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

+
+
+ + + + + The playlist which details have changed. + + + +

Indicates that either the Name or Icon attribute of a + playlist has changed. +

+

Client implementations should be aware that this signal + may not be implemented. +

+ + 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 + +
+
+ +
+
+ + diff --git a/core/dbus/org.mpris.MediaPlayer2.TrackList.xml b/core/dbus/org.mpris.MediaPlayer2.TrackList.xml new file mode 100644 index 0000000..6c511df --- /dev/null +++ b/core/dbus/org.mpris.MediaPlayer2.TrackList.xml @@ -0,0 +1,349 @@ + + + + + +

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

+

+ Example use cases are the list of tracks from the same album as the + currently playing song or the + Rhythmbox play queue. +

+

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

+

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

+

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

+
+ + + +

A mapping from metadata attribute names to values.

+

+ The mpris:trackid 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. +

+

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

+

+ 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 + Xesam ontology + should be used, prefixed by "xesam:". See the + metadata page on the freedesktop.org wiki + for a list of common fields. +

+

+ 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). +

+
+ + +

+ The name of the attribute; see the + metadata page + for guidelines on names to use. +

+
+
+ + +

The value of the attribute, in the most appropriate format.

+
+
+
+ + + +

A unique resource identifier.

+
+
+ + + + +

The list of track ids for which metadata is requested.

+
+
+ + +

Metadata of the set of tracks given as input.

+

See the type documentation for more details.

+
+
+ +

Gets all the metadata available for a set of tracks.

+

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

+
+
+ + + + +

+ The uri of the item to add. Its uri scheme should be an element of the + org.mpris.MediaPlayer2.SupportedUriSchemes + property and the mime-type should match one of the elements of the + org.mpris.MediaPlayer2.SupportedMimeTypes +

+
+
+ + +

+ The identifier of the track after which + the new item should be inserted. The path + /org/mpris/MediaPlayer2/TrackList/NoTrack + indicates that the track should be inserted at the + start of the track list. +

+
+
+ + +

+ Whether the newly inserted track should be considered as + the current track. Setting this to true has the same effect as + calling GoTo afterwards. +

+
+
+ +

Adds a URI in the TrackList.

+

+ If the CanEditTracks property is false, + this has no effect. +

+

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

+
+
+ + + + +

Identifier of the track to be removed.

+

+ /org/mpris/MediaPlayer2/TrackList/NoTrack + is not a valid value for this argument. +

+
+
+ +

Removes an item from the TrackList.

+

If the track is not part of this tracklist, this has no effect.

+

+ If the CanEditTracks property is false, + this has no effect. +

+

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

+
+
+ + + + +

Identifier of the track to skip to.

+

+ /org/mpris/MediaPlayer2/TrackList/NoTrack + is not a valid value for this argument. +

+
+
+ +

Skip to the specified TrackId.

+

If the track is not part of this tracklist, this has no effect.

+

+ If this object is not /org/mpris/MediaPlayer2, + the current TrackList's tracks should be replaced with the contents of + this TrackList, and the TrackListReplaced signal should be fired from + /org/mpris/MediaPlayer2. +

+
+
+ + + + +

+ An array which contains the identifier of each track + in the tracklist, in order. +

+

+ The org.freedesktop.DBus.Properties.PropertiesChanged + 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 + TrackAdded, + TrackRemoved and + TrackListReplaced signals to keep their + representation of the tracklist up to date. +

+
+
+ + + + +

+ If false, calling + AddTrack or + RemoveTrack will have no effect, + and may raise a NotSupported error. +

+
+
+ + + + +

The new content of the tracklist.

+
+
+ + +

The identifier of the track to be considered as current.

+

+ /org/mpris/MediaPlayer2/TrackList/NoTrack + indicates that there is no current track. +

+

+ This should correspond to the mpris:trackid field of the + Metadata property of the org.mpris.MediaPlayer2.Player + interface. +

+
+
+ +

Indicates that the entire tracklist has been replaced.

+

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

+
+
+ + + + +

The metadata of the newly added item.

+

This must include a mpris:trackid entry.

+

See the type documentation for more details.

+
+
+ + +

+ The identifier of the track after which the new track + was inserted. The path + /org/mpris/MediaPlayer2/TrackList/NoTrack + indicates that the track was inserted at the + start of the track list. +

+
+
+ +

Indicates that a track has been added to the track list.

+
+
+ + + + +

The identifier of the track being removed.

+

+ /org/mpris/MediaPlayer2/TrackList/NoTrack + is not a valid value for this argument. +

+
+
+ +

Indicates that a track has been removed from the track list.

+
+
+ + + + +

The id of the track which metadata has changed.

+

If the track id has changed, this will be the old value.

+

+ /org/mpris/MediaPlayer2/TrackList/NoTrack + is not a valid value for this argument. +

+
+
+ + +

The new track metadata.

+

+ This must include a mpris:trackid entry. If the track id has + changed, this will be the new value. +

+

See the type documentation for more details.

+
+
+ +

+ Indicates that the metadata of a track in the tracklist has changed. +

+

+ This may indicate that a track has been replaced, in which case the + mpris:trackid metadata entry is different from the TrackId argument. +

+
+
+ +
+
+ diff --git a/core/dbus/org.mpris.MediaPlayer2.xml b/core/dbus/org.mpris.MediaPlayer2.xml new file mode 100644 index 0000000..aca33e6 --- /dev/null +++ b/core/dbus/org.mpris.MediaPlayer2.xml @@ -0,0 +1,198 @@ + + + + + + + +

+ Brings the media player's user interface to the front using any + appropriate mechanism available. +

+

+ 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 CanRaise property is + false and this method does nothing. +

+
+
+ + + +

Causes the media player to stop running.

+

+ The media player may refuse to allow clients to shut it down. + In this case, the CanQuit property is + false and this method does nothing. +

+

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

+

If the media player does not have a UI, this should be implemented.

+
+
+ + + +

+ If false, calling + Quit will have no effect, and may + raise a NotSupported error. If true, calling + Quit will cause the media application + to attempt to quit (although it may still be prevented from quitting + by the user, for example). +

+
+
+ + + + + +

Whether the media player is occupying the fullscreen.

+

+ This is typically used for videos. A value of true + indicates that the media player is taking up the full screen. +

+

+ Media centre software may well have this value fixed to true +

+

+ If CanSetFullscreen is true, + clients may set this property to true to tell the media player + to enter fullscreen mode, or to false to return to windowed + mode. +

+

+ If CanSetFullscreen is false, + then attempting to set this property should have no effect, and may raise + an error. However, even if it is true, 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). +

+ +

+ This allows remote control interfaces, such as LIRC or mobile devices like + phones, to control whether a video is shown in fullscreen. +

+
+
+
+ + + + + +

+ If false, attempting to set + Fullscreen will have no effect, and may + raise an error. If true, attempting to set + Fullscreen 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. +

+

+ 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 false. +

+ +

+ This allows clients to choose whether to display controls for entering + or exiting fullscreen mode. +

+
+
+
+ + + +

+ If false, calling + Raise will have no effect, and may + raise a NotSupported error. If true, calling + Raise 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). +

+
+
+ + + +

+ Indicates whether the /org/mpris/MediaPlayer2 + object implements the org.mpris.MediaPlayer2.TrackList + interface. +

+
+
+ + + +

A friendly name to identify the media player to users.

+

This should usually match the name found in .desktop files

+

(eg: "VLC media player").

+
+
+ + + + +

The basename of an installed .desktop file which complies with the Desktop entry specification, + with the ".desktop" extension stripped.

+

+ Example: The desktop entry file is "/usr/share/applications/vlc.desktop", + and this property contains "vlc" +

+
+
+ + + +

+ The URI schemes supported by the media player. +

+

+ 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". +

+

+ Note that URI schemes should be lower-case. +

+ +

+ This is important for clients to know when using the editing + capabilities of the Playlist interface, for example. +

+
+
+
+ + + +

+ The mime-types supported by the media player. +

+

+ Mime-types should be in the standard format (eg: audio/mpeg or + application/ogg). +

+ +

+ This is important for clients to know when using the editing + capabilities of the Playlist interface, for example. +

+
+
+
+ +
+
+ diff --git a/core/include/JellyfinQt/jellyfin.h b/core/include/JellyfinQt/jellyfin.h index 4cc4612..d6aedf3 100644 --- a/core/include/JellyfinQt/jellyfin.h +++ b/core/include/JellyfinQt/jellyfin.h @@ -22,6 +22,7 @@ #include #include "model/item.h" +#include "dto/itemfields.h" #include "dto/mediastream.h" #include "dto/nameguidpair.h" #include "dto/userdto.h" @@ -36,6 +37,7 @@ #include "viewmodel/loader.h" #include "viewmodel/mediastream.h" #include "viewmodel/modelstatus.h" +#include "viewmodel/platformmediacontrol.h" #include "viewmodel/playbackmanager.h" #include "viewmodel/playlist.h" #include "viewmodel/userdata.h" diff --git a/core/include/JellyfinQt/model/playlist.h b/core/include/JellyfinQt/model/playlist.h index a33030e..32523db 100644 --- a/core/include/JellyfinQt/model/playlist.h +++ b/core/include/JellyfinQt/model/playlist.h @@ -58,11 +58,15 @@ public: */ int currentItemIndexInList() const; + bool hasPrevious(); + /** * @brief Determine the previous item to be played. */ void previous(); + bool hasNext(); + /** * @brief Determine the next item to be played. */ diff --git a/core/include/JellyfinQt/model/shuffle.h b/core/include/JellyfinQt/model/shuffle.h index 8c5b66d..bcaacc4 100644 --- a/core/include/JellyfinQt/model/shuffle.h +++ b/core/include/JellyfinQt/model/shuffle.h @@ -83,6 +83,9 @@ public: */ virtual int nextItem() const { return -1; } + virtual bool hasPrevious() const { return false; } + virtual bool hasNext() const { return false; } + /** * @brief Sets whether the shuffler to loop over the list if all items are played. */ @@ -108,6 +111,8 @@ public: virtual void previous() override; virtual void next() override; virtual void setIndex(int i) override; + virtual bool hasPrevious() const override; + virtual bool hasNext() const override; protected: int nextIndex() const; int previousIndex() const; @@ -148,6 +153,8 @@ public: virtual int nextItem() const override; virtual void previous() override; virtual void next() override; + virtual bool hasPrevious() const override; + virtual bool hasNext() const override; protected: int m_previous, m_current, m_next = -1; }; diff --git a/core/include/JellyfinQt/platform/freedesktop/mediaplayer2.h b/core/include/JellyfinQt/platform/freedesktop/mediaplayer2.h new file mode 100644 index 0000000..d4e5e56 --- /dev/null +++ b/core/include/JellyfinQt/platform/freedesktop/mediaplayer2.h @@ -0,0 +1,118 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp org.mpris.MediaPlayer2.xml -a ../include/JellyfinQt/platform/freedesktop/mediaplayer2.h:../src/platform/freedesktop/mediaplayer2.cpp + * + * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. + * + * This is an auto-generated file. + * This file may have been hand-edited. Look for HAND-EDIT comments + * before re-generating it. + */ + +//HAND-EDIT: include-guard +#ifndef JELLYFIN_PLATFORM_FREEDESKTOP_MEDIAPLAYER2_H +#define JELLYFIN_PLATFORM_FREEDESKTKOP_MEDIAPLAYER2_H + +#include +#include +QT_BEGIN_NAMESPACE +class QByteArray; +template class QList; +template class QMap; +class QString; +class QStringList; +class QVariant; +QT_END_NAMESPACE + +//HAND-EDIT: added namespaces +namespace Jellyfin { +namespace ViewModel { + class PlatformMediaControl; +} + +namespace Platform { +namespace FreeDesktop { + +/* + * Adaptor class for interface org.mpris.MediaPlayer2 + */ +class MediaPlayer2Adaptor: public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2") + Q_CLASSINFO("D-Bus Introspection", "" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" + "") +public: + MediaPlayer2Adaptor(ViewModel::PlatformMediaControl *parent); + virtual ~MediaPlayer2Adaptor(); + +public: // PROPERTIES + Q_PROPERTY(bool CanQuit READ canQuit) + bool canQuit() const; + + Q_PROPERTY(bool CanRaise READ canRaise) + bool canRaise() const; + + Q_PROPERTY(bool CanSetFullscreen READ canSetFullscreen) + bool canSetFullscreen() const; + + Q_PROPERTY(QString DesktopEntry READ desktopEntry) + QString desktopEntry() const; + + Q_PROPERTY(bool Fullscreen READ fullscreen WRITE setFullscreen) + bool fullscreen() const; + void setFullscreen(bool value); + + Q_PROPERTY(bool HasTrackList READ hasTrackList) + bool hasTrackList() const; + + Q_PROPERTY(QString Identity READ identity) + QString identity() const; + + Q_PROPERTY(QStringList SupportedMimeTypes READ supportedMimeTypes) + QStringList supportedMimeTypes() const; + + Q_PROPERTY(QStringList SupportedUriSchemes READ supportedUriSchemes) + QStringList supportedUriSchemes() const; + +public Q_SLOTS: // METHODS + void Quit(); + void Raise(); +Q_SIGNALS: // SIGNALS +private: + ViewModel::PlatformMediaControl *m_mediaControl; +}; + +} // NS FreeDesktop +} // NS Platform +} // NS Jellyfin + +#endif diff --git a/core/include/JellyfinQt/platform/freedesktop/mediaplayer2player.h b/core/include/JellyfinQt/platform/freedesktop/mediaplayer2player.h new file mode 100644 index 0000000..5e0f95c --- /dev/null +++ b/core/include/JellyfinQt/platform/freedesktop/mediaplayer2player.h @@ -0,0 +1,205 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp org.mpris.MediaPlayer2.Player.xml -a ../include/JellyfinQt/platform/freedesktop/mediaplayer2player.h:../src/platform/freedesktop/mediaplayer2player.cpp + * + * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. + * + * This is an auto-generated file. + * This file may have been hand-edited. Look for HAND-EDIT comments + * before re-generating it. + */ + +//HAND-EDIT: include-guard +#ifndef JELLYFIN_PLATFORM_FREEDESKTOP_MEDIAPLAYER2PLAYER_H +#define JELLYFIN_PLATFORM_FREEDESKTOP_MEDIAPLAYER2PLAYER_H + +#include +#include +#include +QT_BEGIN_NAMESPACE +class QByteArray; +template class QList; +template class QMap; +class QString; +class QStringList; +class QVariant; +QT_END_NAMESPACE + +//HAND-EDIT: added namespaces +namespace Jellyfin { +namespace ViewModel { + class Item; + class PlatformMediaControl; + class PlaybackManager; +} + +namespace Platform { +namespace FreeDesktop { +/* + * Adaptor class for interface org.mpris.MediaPlayer2.Player + */ +class PlayerAdaptor: public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2.Player") + Q_CLASSINFO("D-Bus Introspection", "" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" + "") +public: + PlayerAdaptor(ViewModel::PlatformMediaControl *parent); + virtual ~PlayerAdaptor(); + +public: // PROPERTIES + Q_PROPERTY(bool CanControl READ canControl) + bool canControl() const; + + Q_PROPERTY(bool CanGoNext READ canGoNext) + bool canGoNext() const; + + Q_PROPERTY(bool CanGoPrevious READ canGoPrevious) + bool canGoPrevious() const; + + Q_PROPERTY(bool CanPause READ canPause) + bool canPause() const; + + Q_PROPERTY(bool CanPlay READ canPlay) + bool canPlay() const; + + Q_PROPERTY(bool CanSeek READ canSeek) + bool canSeek() const; + + Q_PROPERTY(QString LoopStatus READ loopStatus WRITE setLoopStatus) + QString loopStatus() const; + void setLoopStatus(const QString &value); + + Q_PROPERTY(double MaximumRate READ maximumRate) + double maximumRate() const; + + Q_PROPERTY(QVariantMap Metadata READ metadata) + QVariantMap metadata() const; + + Q_PROPERTY(double MinimumRate READ minimumRate) + double minimumRate() const; + + Q_PROPERTY(QString PlaybackStatus READ playbackStatus) + QString playbackStatus() const; + + Q_PROPERTY(qlonglong Position READ position) + qlonglong position() const; + + Q_PROPERTY(double Rate READ rate WRITE setRate) + double rate() const; + void setRate(double value); + + Q_PROPERTY(bool Shuffle READ shuffle WRITE setShuffle) + bool shuffle() const; + void setShuffle(bool value); + + Q_PROPERTY(double Volume READ volume WRITE setVolume) + double volume() const; + void setVolume(double value); + +public Q_SLOTS: // METHODS + void Next(); + void OpenUri(const QString &Uri); + void Pause(); + void Play(); + void PlayPause(); + void Previous(); + void Seek(qlonglong Offset); + void SetPosition(const QDBusObjectPath &TrackId, qlonglong Position); + void Stop(); +Q_SIGNALS: // SIGNALS + void Seeked(qlonglong Position); + +private: + ViewModel::PlatformMediaControl *m_mediaControl; + void notifyPropertiesChanged(QStringList properties); +private slots: + void onCurrentItemChanged(ViewModel::Item *newItem); + void onPlaybackStateChanged(QMediaPlayer::State state); + void onMediaStatusChanged(QMediaPlayer::MediaStatus status); + void onPositionChanged(qint64 position); + void onSeekableChanged(bool seekable); + void onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlaybackManager); +}; + +} // NS FreeDesktop +} // NS Platform +} // NS Jellyfin + +#endif diff --git a/core/include/JellyfinQt/viewmodel/platformmediacontrol.h b/core/include/JellyfinQt/viewmodel/platformmediacontrol.h new file mode 100644 index 0000000..13fbe14 --- /dev/null +++ b/core/include/JellyfinQt/viewmodel/platformmediacontrol.h @@ -0,0 +1,127 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#ifndef JELLYFIN_VIEWMODEL_PLATFORMMEDIACONTROL_H +#define JELLYFIN_VIEWMODEL_PLATFORMMEDIACONTROL_H + +#include +#include +#include + +namespace Jellyfin { +namespace ViewModel { + +class PlatformMediaControlPrivate; +class PlaybackManager; + +/** + * @brief Exposes media control and information to the OS. Uses MPRIS on FreeDesktop-enabled systems. + */ +class PlatformMediaControl : public QObject, public QQmlParserStatus { + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) +public: + explicit PlatformMediaControl(QObject *parent = nullptr); + + Q_PROPERTY(Jellyfin::ViewModel::PlaybackManager *playbackManager READ playbackManager WRITE setPlaybackManager NOTIFY playbackManagerChanged) + /** + * Whether the operating system can request the media player to quit. If set, + * the quitRequested signal may be emitted and the application should quit. + */ + Q_PROPERTY(bool canQuit READ canQuit WRITE setCanQuit NOTIFY canQuitChanged) + Q_PROPERTY(bool canRaise READ canRaise WRITE setCanRaise NOTIFY canRaiseChanged) + Q_PROPERTY(QString playerName READ playerName WRITE setPlayerName NOTIFY playerNameChanged) + Q_PROPERTY(QString desktopFile READ playerName WRITE setPlayerName NOTIFY playerNameChanged) + + PlaybackManager *playbackManager() const { return m_playbackManager; }; + + void setPlaybackManager(PlaybackManager *newPlaybackManager) { + m_playbackManager = newPlaybackManager; + emit playbackManagerChanged(newPlaybackManager); + }; + + bool canQuit() const { return m_canQuit; }; + void setCanQuit(bool newCanQuit) { + m_canQuit = newCanQuit; + emit canQuitChanged(newCanQuit); + } + + void requestQuit() { + emit quitRequested(); + } + + bool canRaise() const { return m_canRaise; }; + void setCanRaise(bool newCanRaise) { + m_canRaise = newCanRaise; + emit canRaiseChanged(newCanRaise); + } + + void requestRaise() { + emit raiseRequested(); + };; + + QString playerName() const { return m_playerName; } + void setPlayerName(QString newPlayerName) { + m_playerName = newPlayerName; + emit playerNameChanged(newPlayerName); + } + + QString desktopFile() const { return m_desktopFile; } + void setDesktopFile(QString newDesktopFile) { + m_desktopFile = newDesktopFile; + emit desktopFileChanged(newDesktopFile); + } + + void classBegin() override { + m_isParsing = true; + } + void componentComplete() override { + m_isParsing = false; + setup(); + } + + + +signals: + void playbackManagerChanged(PlaybackManager *newPlaybackManager); + void canQuitChanged(bool newCanQuit); + void canRaiseChanged(bool newCanRaise); + void playerNameChanged(QString newPlayerName); + void desktopFileChanged(QString newDesktopFile); + + void quitRequested(); + void raiseRequested(); + +private: + Q_DECLARE_PRIVATE(PlatformMediaControl) + PlatformMediaControlPrivate* d_ptr; + void setup(); + bool m_isParsing = false; + + PlaybackManager *m_playbackManager = nullptr; + bool m_canQuit = false; + bool m_canRaise = false; + QString m_playerName = QStringLiteral("JellyfinQt"); + QString m_desktopFile = QStringLiteral("sailfin"); +}; + + +} // NS ViewModel +} // NS Jellyfin + +#endif // PLATFORMMEDIACONTROL_H diff --git a/core/include/JellyfinQt/viewmodel/playbackmanager.h b/core/include/JellyfinQt/viewmodel/playbackmanager.h index be324a3..39173fc 100644 --- a/core/include/JellyfinQt/viewmodel/playbackmanager.h +++ b/core/include/JellyfinQt/viewmodel/playbackmanager.h @@ -97,8 +97,12 @@ public: Q_PROPERTY(QMediaPlayer::MediaStatus mediaStatus READ mediaStatus NOTIFY mediaStatusChanged) Q_PROPERTY(QMediaPlayer::State playbackState READ playbackState NOTIFY playbackStateChanged) Q_PROPERTY(qint64 position READ position NOTIFY positionChanged) + Q_PROPERTY(bool hasNext READ hasNext NOTIFY hasNextChanged) + Q_PROPERTY(bool hasPrevious READ hasPrevious NOTIFY hasPreviousChanged) ViewModel::Item *item() const { return m_displayItem; } + QSharedPointer dataItem() const { return m_item; } + ApiClient *apiClient() const { return m_apiClient; } void setApiClient(ApiClient *apiClient); QString streamUrl() const { return m_streamUrl; } @@ -108,6 +112,8 @@ public: qint64 duration() const { return m_mediaPlayer->duration(); } ViewModel::Playlist *queue() const { return m_displayQueue; } int queueIndex() const { return m_queueIndex; } + bool hasNext() const { return m_queue->hasNext(); } + bool hasPrevious() const { return m_queue->hasPrevious(); } // Current media player related property getters QMediaPlayer::State playbackState() const { return m_mediaPlayer->state()/*m_playbackState*/; } @@ -138,6 +144,8 @@ signals: void seekableChanged(bool newSeekable); void errorChanged(QMediaPlayer::Error newError); void errorStringChanged(const QString &newErrorString); + void hasNextChanged(bool newHasNext); + void hasPreviousChanged(bool newHasPrevious); public slots: /** * @brief playItem Replaces the current queue and plays the item with the given id. diff --git a/core/src/jellyfin.cpp b/core/src/jellyfin.cpp index 9b30de5..45f6753 100644 --- a/core/src/jellyfin.cpp +++ b/core/src/jellyfin.cpp @@ -23,6 +23,7 @@ void registerTypes(const char *uri) { qmlRegisterType(uri, 1, 0, "ApiClient"); qmlRegisterType(uri, 1, 0, "ServerDiscoveryModel"); qmlRegisterType(uri, 1, 0, "PlaybackManager"); + qmlRegisterType(uri, 1, 0, "PlatformMediaControl"); qmlRegisterUncreatableType(uri, 1, 0, "Item", "Acquire one via ItemLoader or exposed properties"); qmlRegisterUncreatableType(uri, 1, 0, "User", "Acquire one via UserLoader or exposed properties"); qmlRegisterUncreatableType(uri, 1, 0, "EventBus", "Obtain one via your ApiClient"); diff --git a/core/src/model/playlist.cpp b/core/src/model/playlist.cpp index 6c37f13..561222b 100644 --- a/core/src/model/playlist.cpp +++ b/core/src/model/playlist.cpp @@ -35,6 +35,10 @@ void Playlist::clearList() { emit listCleared(); } +bool Playlist::hasPrevious() { + return m_shuffler->hasPrevious(); +} + void Playlist::previous() { m_shuffler->previous(); int curItem = m_shuffler->currentItem(); @@ -54,6 +58,10 @@ void Playlist::previous() { emit currentItemChanged(); } +bool Playlist::hasNext() { + return m_shuffler->hasNext(); +} + void Playlist::next() { // Determine the new current item if (!m_queue.isEmpty()) { diff --git a/core/src/model/shuffle.cpp b/core/src/model/shuffle.cpp index b6ee65b..831af08 100644 --- a/core/src/model/shuffle.cpp +++ b/core/src/model/shuffle.cpp @@ -44,6 +44,14 @@ void NoShuffle::previous() { m_index = previousIndex(); } +bool NoShuffle::hasPrevious() const { + return m_index > 0; +} + +bool NoShuffle::hasNext() const { + return m_index < m_playlist->listSize() - 1; +} + int NoShuffle::currentItem() const { return m_index; } @@ -150,5 +158,13 @@ void RandomShuffle::next() { m_next = random(m_playlist->listSize()); } +bool RandomShuffle::hasPrevious() const { + return true; +} + +bool RandomShuffle::hasNext() const { + return true; +} + } // NS Model } // NS Jellyfin diff --git a/core/src/platform/freedesktop/mediaplayer2.cpp b/core/src/platform/freedesktop/mediaplayer2.cpp new file mode 100644 index 0000000..7f67859 --- /dev/null +++ b/core/src/platform/freedesktop/mediaplayer2.cpp @@ -0,0 +1,117 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp org.mpris.MediaPlayer2.xml -a ../include/JellyfinQt/platform/freedesktop/mediaplayer2.h:../src/platform/freedesktop/mediaplayer2.cpp + * + * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. + * + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + +#include "JellyfinQt/platform/freedesktop/mediaplayer2.h" +#include "JellyfinQt/viewmodel/platformmediacontrol.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Jellyfin { +namespace Platform { +namespace FreeDesktop { + +/* + * Implementation of adaptor class MediaPlayer2Adaptor + */ + +MediaPlayer2Adaptor::MediaPlayer2Adaptor(ViewModel::PlatformMediaControl *parent) + : QDBusAbstractAdaptor(parent), + m_mediaControl(parent) +{ + // constructor + setAutoRelaySignals(true); +} + +MediaPlayer2Adaptor::~MediaPlayer2Adaptor() +{ + // destructor +} + +bool MediaPlayer2Adaptor::canQuit() const +{ + // get the value of property CanQuit + return m_mediaControl->canQuit(); +} + +bool MediaPlayer2Adaptor::canRaise() const +{ + // get the value of property CanRaise + return m_mediaControl->canRaise(); +} + +bool MediaPlayer2Adaptor::canSetFullscreen() const +{ + // get the value of property CanSetFullscreen + return qvariant_cast< bool >(parent()->property("CanSetFullscreen")); +} + +QString MediaPlayer2Adaptor::desktopEntry() const +{ + // get the value of property DesktopEntry + return m_mediaControl->desktopFile(); +} + +bool MediaPlayer2Adaptor::fullscreen() const +{ + // get the value of property Fullscreen + return qvariant_cast< bool >(parent()->property("Fullscreen")); +} + +void MediaPlayer2Adaptor::setFullscreen(bool value) +{ + // set the value of property Fullscreen + parent()->setProperty("Fullscreen", QVariant::fromValue(value)); +} + +bool MediaPlayer2Adaptor::hasTrackList() const +{ + // get the value of property HasTrackList + //return qvariant_cast< bool >(parent()->property("HasTrackList")); + return false; +} + +QString MediaPlayer2Adaptor::identity() const +{ + // get the value of property Identity + return m_mediaControl->playerName(); +} + +QStringList MediaPlayer2Adaptor::supportedMimeTypes() const +{ + // get the value of property SupportedMimeTypes + return qvariant_cast< QStringList >(parent()->property("SupportedMimeTypes")); +} + +QStringList MediaPlayer2Adaptor::supportedUriSchemes() const +{ + // get the value of property SupportedUriSchemes + QStringList supportedUriSchemes; + supportedUriSchemes << "urn"; + return supportedUriSchemes; +} + +void MediaPlayer2Adaptor::Quit() +{ + m_mediaControl->requestQuit(); +} + +void MediaPlayer2Adaptor::Raise() +{ + m_mediaControl->requestRaise(); +} + +} // NS FreeDesktop +} // NS Platform +} // NS Jellyfin diff --git a/core/src/platform/freedesktop/mediaplayer2player.cpp b/core/src/platform/freedesktop/mediaplayer2player.cpp new file mode 100644 index 0000000..9a477cb --- /dev/null +++ b/core/src/platform/freedesktop/mediaplayer2player.cpp @@ -0,0 +1,325 @@ +/* + * This file was generated by qdbusxml2cpp version 0.8 + * Command line was: qdbusxml2cpp org.mpris.MediaPlayer2.Player.xml -a ../include/JellyfinQt/platform/freedesktop/mediaplayer2player.h:../src/platform/freedesktop/mediaplayer2player.cpp + * + * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. + * + * This is an auto-generated file. + * Do not edit! All changes made to it will be lost. + */ + +#include "JellyfinQt/platform/freedesktop/mediaplayer2player.h" +#include "JellyfinQt/viewmodel/item.h" +#include "JellyfinQt/viewmodel/platformmediacontrol.h" +#include "JellyfinQt/viewmodel/playbackmanager.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Jellyfin { +namespace Platform { +namespace FreeDesktop { + +/* + * Implementation of adaptor class PlayerAdaptor + */ + +PlayerAdaptor::PlayerAdaptor(ViewModel::PlatformMediaControl *parent) + : QDBusAbstractAdaptor(parent), + m_mediaControl(parent) { + // constructor + //setAutoRelaySignals(true); + onPlaybackManagerChanged(m_mediaControl->playbackManager()); + connect(m_mediaControl, &ViewModel::PlatformMediaControl::playbackManagerChanged, this, &PlayerAdaptor::onPlaybackManagerChanged); + /*if (m_mediaControl != nullptr && m_mediaControl->playbackManager() != nullptr) { + }*/ +} + +PlayerAdaptor::~PlayerAdaptor() { + // destructor +} + +bool PlayerAdaptor::canControl() const +{ + // get the value of property CanControl + return true; +} + +bool PlayerAdaptor::canGoNext() const +{ + // get the value of property CanGoNext + return canPlay() && m_mediaControl->playbackManager()->hasNext(); +} + +bool PlayerAdaptor::canGoPrevious() const +{ + // get the value of property CanGoPrevious + return canPlay() && m_mediaControl->playbackManager()->hasPrevious(); +} + +bool PlayerAdaptor::canPause() const +{ + // get the value of property CanPause + return canPlay(); +} + +bool PlayerAdaptor::canPlay() const +{ + // get the value of property CanPlay + return m_mediaControl->playbackManager()->queue()->rowCount(QModelIndex()) > 0; +} + +bool PlayerAdaptor::canSeek() const +{ + // get the value of property CanSeek + return m_mediaControl->playbackManager()->seekable(); +} + +QString PlayerAdaptor::loopStatus() const +{ + // get the value of property LoopStatus + return "None"; +} + +void PlayerAdaptor::setLoopStatus(const QString &value) +{ + // set the value of property LoopStatus + parent()->setProperty("LoopStatus", QVariant::fromValue(value)); +} + +double PlayerAdaptor::maximumRate() const +{ + // get the value of property MaximumRate + return 1.0; +} + +QVariantMap PlayerAdaptor::metadata() const +{ + // get the value of property Metadata + QVariantMap map; + if (m_mediaControl->playbackManager() == nullptr || m_mediaControl->playbackManager()->dataItem().isNull()) { + return map; + } + ViewModel::PlaybackManager *plybkMgr = m_mediaControl->playbackManager(); + + QSharedPointer item = plybkMgr->dataItem(); + if (!item.isNull()) { + map[QStringLiteral("mpris:trackid")] = QVariant::fromValue(QDBusObjectPath(QStringLiteral("/nl/netsoj/chris/jellyfinqt/item/").append(item->jellyfinId()))); + if (item->runTimeTicks().has_value()) { + map[QStringLiteral("mpris:length")] = item->runTimeTicks().value() / 10; + } + map[QStringLiteral("xesam:title")] = item->name(); + if (!item->albumPrimaryImageTagNull()) { + map[QStringLiteral("mpris:artUrl")] = QStringLiteral("%1/Items/%2/Images/Primary?tag=%3").arg(plybkMgr->apiClient()->baseUrl(), + item->jellyfinId(), item->albumPrimaryImageTag()); + } + + QStringList albumArtists; + QList tmp = item->albumArtists(); + for (auto it = tmp.cbegin(); it != tmp.cend(); it++) { + albumArtists << it->name(); + } + map[QStringLiteral("xesam:albumArtist")] = albumArtists; + map[QStringLiteral("xesam:album")] = item->album(); + map[QStringLiteral("xesam:artist")] = item->artists(); + if (item->parentIndexNumber().has_value()) { + map[QStringLiteral("xesam:discNumber")] = item->parentIndexNumber().value(); + } + if (item->indexNumber().has_value()) { + map[QStringLiteral("xesam:trackNumber")] = item->indexNumber().value(); + } + map[QStringLiteral("xesam:contentCreated")] = item->dateCreated(); + map[QStringLiteral("xesam:genre")] = item->genres(); + map[QStringLiteral("xesam:lastUsed")] = item->userData()->lastPlayedDate(); + } + return map; +} + +double PlayerAdaptor::minimumRate() const +{ + // get the value of property MinimumRate + return 1.0; +} + +QString PlayerAdaptor::playbackStatus() const +{ + // get the value of property PlaybackStatus + if (m_mediaControl == nullptr || m_mediaControl->playbackManager() == nullptr) { + return "Stopped"; + } + switch(m_mediaControl->playbackManager()->playbackState()) { + case QMediaPlayer::StoppedState: + return "Stopped"; + case QMediaPlayer::PlayingState: + return "Playing"; + case QMediaPlayer::PausedState: + return "Paused"; + default: + return "Stopped"; + } +} + +qlonglong PlayerAdaptor::position() const +{ + // get the value of property Position + return m_mediaControl->playbackManager()->position() * 1000; +} + +double PlayerAdaptor::rate() const +{ + // get the value of property Rate + return 1.0; +} + +void PlayerAdaptor::setRate(double value) +{ + // set the value of property Rate + parent()->setProperty("Rate", QVariant::fromValue(value)); +} + +bool PlayerAdaptor::shuffle() const +{ + // get the value of property Shuffle + return false; +} + +void PlayerAdaptor::setShuffle(bool value) +{ + // set the value of property Shuffle + parent()->setProperty("Shuffle", QVariant::fromValue(value)); +} + +double PlayerAdaptor::volume() const +{ + // get the value of property Volume + return qvariant_cast< double >(parent()->property("Volume")); +} + +void PlayerAdaptor::setVolume(double value) +{ + // set the value of property Volume + parent()->setProperty("Volume", QVariant::fromValue(value)); +} + +void PlayerAdaptor::Next() +{ + // handle method call org.mpris.MediaPlayer2.Player.Next + m_mediaControl->playbackManager()->next(); +} + +void PlayerAdaptor::OpenUri(const QString &Uri) +{ + // handle method call org.mpris.MediaPlayer2.Player.OpenUri + QMetaObject::invokeMethod(parent(), "OpenUri", Q_ARG(QString, Uri)); +} + +void PlayerAdaptor::Pause() +{ + // handle method call org.mpris.MediaPlayer2.Player.Pause + m_mediaControl->playbackManager()->pause(); +} + +void PlayerAdaptor::Play() +{ + // handle method call org.mpris.MediaPlayer2.Player.Play + m_mediaControl->playbackManager()->play(); +} + +void PlayerAdaptor::PlayPause() +{ + // handle method call org.mpris.MediaPlayer2.Player.PlayPause + if (m_mediaControl->playbackManager()->playbackState() == QMediaPlayer::PlayingState) { + m_mediaControl->playbackManager()->pause(); + } else { + m_mediaControl->playbackManager()->play(); + } +} + +void PlayerAdaptor::Previous() +{ + // handle method call org.mpris.MediaPlayer2.Player.Previous + m_mediaControl->playbackManager()->previous(); +} + +void PlayerAdaptor::Seek(qlonglong Offset) +{ + // handle method call org.mpris.MediaPlayer2.Player.Seek + m_mediaControl->playbackManager()->seek(m_mediaControl->playbackManager()->position() + Offset / 1000); +} + +void PlayerAdaptor::SetPosition(const QDBusObjectPath &TrackId, qlonglong Position) +{ + // handle method call org.mpris.MediaPlayer2.Player.SetPosition + if (TrackId.path() == QStringLiteral("/nl/netsoj/chris/jellyfinqt/item/").append(m_mediaControl->playbackManager()->dataItem()->jellyfinId())) { + m_mediaControl->playbackManager()->seek(Position / 1000); + } +} + +void PlayerAdaptor::Stop() +{ + // handle method call org.mpris.MediaPlayer2.Player.Stop + QMetaObject::invokeMethod(parent(), "Stop"); +} +void PlayerAdaptor::notifyPropertiesChanged(QStringList properties) { + QDBusMessage signal = QDBusMessage::createSignal("/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties", "PropertiesChanged"); + signal << "org.mpris.MediaPlayer2.Player"; // 1st argument: interface name + QVariantMap changedProperties; + for (auto it = properties.cbegin(); it != properties.cend(); it++) { + changedProperties[*it] = property(it->toLocal8Bit().data()); + } + signal << changedProperties; // 2nd argument: changed properties + signal << QStringList(); // 3th argument: invalidated properties + QDBusConnection::sessionBus().send(signal); +} + +void PlayerAdaptor::onCurrentItemChanged(ViewModel::Item *item) { + Q_UNUSED(item) + + QStringList properties; + properties << "Metadata" << "Position" << "CanPlay" << "CanPause" << "CanGoNext" << "CanGoPrevious"; + notifyPropertiesChanged(properties); +} +void PlayerAdaptor::onPlaybackStateChanged(QMediaPlayer::State state) { + Q_UNUSED(state) + QStringList properties; + properties << "PlaybackStatus" << "Position"; + notifyPropertiesChanged(properties); + +} + +void PlayerAdaptor::onMediaStatusChanged(QMediaPlayer::MediaStatus status) { + Q_UNUSED(status) + QStringList properties; + properties << "PlaybackStatus" << "Position"; + notifyPropertiesChanged(properties); +} + +void PlayerAdaptor::onPositionChanged(qint64 position) { + Q_UNUSED(position) + /*QStringList properties; + properties << "Position"; + notifyPropertiesChanged(properties);*/ +} +void PlayerAdaptor::onSeekableChanged(bool seekable) { + QStringList properties; + properties << "CanSeek"; + notifyPropertiesChanged(properties); +} + +void PlayerAdaptor::onPlaybackManagerChanged(ViewModel::PlaybackManager *newPlaybackManager) { + if (newPlaybackManager != nullptr) { + connect(newPlaybackManager, &ViewModel::PlaybackManager::itemChanged, this, &PlayerAdaptor::onCurrentItemChanged); + connect(newPlaybackManager, &ViewModel::PlaybackManager::playbackStateChanged, this, &PlayerAdaptor::onPlaybackStateChanged); + connect(newPlaybackManager, &ViewModel::PlaybackManager::mediaStatusChanged, this, &PlayerAdaptor::onMediaStatusChanged); + connect(newPlaybackManager, &ViewModel::PlaybackManager::positionChanged, this, &PlayerAdaptor::onPositionChanged); + connect(newPlaybackManager, &ViewModel::PlaybackManager::seekableChanged, this, &PlayerAdaptor::onSeekableChanged); + } +} + +} // NS FreeDesktop +} // NS Platform +} // NS Jellyfin diff --git a/core/src/viewmodel/platformmediacontrol_freedesktop.cpp b/core/src/viewmodel/platformmediacontrol_freedesktop.cpp new file mode 100644 index 0000000..d6548ed --- /dev/null +++ b/core/src/viewmodel/platformmediacontrol_freedesktop.cpp @@ -0,0 +1,87 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "JellyfinQt/viewmodel/platformmediacontrol.h" +#include "JellyfinQt/platform/freedesktop/mediaplayer2.h" +#include "JellyfinQt/platform/freedesktop/mediaplayer2player.h" + +#include +#include +#include + +namespace Jellyfin { +namespace ViewModel { + +using Platform::FreeDesktop::MediaPlayer2Adaptor; +using Platform::FreeDesktop::PlayerAdaptor; + +class PlatformMediaControlPrivate { +public: + PlatformMediaControlPrivate(PlatformMediaControl *parent); + void setupConnection(); +private: + PlatformMediaControl *q_ptr; + Q_DECLARE_PUBLIC(PlatformMediaControl) + + MediaPlayer2Adaptor *m_mainAdaptor; + PlayerAdaptor *m_playerAdaptor; + QDBusConnection m_connection; +public slots: + // MPRIS Player methods + void Quit(); + +}; + +PlatformMediaControl::PlatformMediaControl(QObject *parent) + : QObject(parent) { + d_ptr = new PlatformMediaControlPrivate(this); +} + +void PlatformMediaControl::setup() { + Q_D(PlatformMediaControl); + d->setupConnection(); +} + +PlatformMediaControlPrivate::PlatformMediaControlPrivate(PlatformMediaControl *parent) + : q_ptr(parent), + m_mainAdaptor(new MediaPlayer2Adaptor(parent)), + m_playerAdaptor(new PlayerAdaptor(parent)), + m_connection(QDBusConnection::sessionBus()) { + +} + +void PlatformMediaControlPrivate::setupConnection() { + Q_Q(PlatformMediaControl); + if(!m_connection.registerObject(QStringLiteral("/org/mpris/MediaPlayer2"), q)) { + qWarning() << "MediaPlayer2 dbus object not registered: " << m_connection.lastError(); + } + if (!m_connection.registerService(QStringLiteral("org.mpris.MediaPlayer2.sailfin.instance").append(QString::number(QCoreApplication::applicationPid())))) { + qWarning() << "Could not aqcuire DBus name: " << m_connection.lastError(); + } +} + +void PlatformMediaControlPrivate::Quit() { + Q_Q(PlatformMediaControl); + q->requestQuit(); +} + + + +} +} diff --git a/core/src/viewmodel/platformmediacontrol_stub.cpp b/core/src/viewmodel/platformmediacontrol_stub.cpp new file mode 100644 index 0000000..241baac --- /dev/null +++ b/core/src/viewmodel/platformmediacontrol_stub.cpp @@ -0,0 +1,29 @@ +/* + * Sailfin: a Jellyfin client written using Qt + * Copyright (C) 2021 Chris Josten and the Sailfin Contributors. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "JellyfinQt/viewmodel/platformmediacontrol.h" + +namespace Jellyfin { +namespace ViewModel { + +PlatformMediaControl::PlatformMediaControl(QObject *parent) + : QObject(parent) {} + +} +} diff --git a/core/src/viewmodel/playbackmanager.cpp b/core/src/viewmodel/playbackmanager.cpp index 59fc5a0..902b531 100644 --- a/core/src/viewmodel/playbackmanager.cpp +++ b/core/src/viewmodel/playbackmanager.cpp @@ -84,6 +84,9 @@ void PlaybackManager::setItem(QSharedPointer newItem) { } emit itemChanged(m_displayItem); + emit hasNextChanged(m_queue->hasNext()); + emit hasPreviousChanged(m_queue->hasPrevious()); + if (m_apiClient == nullptr) { qWarning() << "apiClient is not set on this MediaSource instance! Aborting."; return; diff --git a/qtquick/qml/main.qml b/qtquick/qml/main.qml index 6e98d8f..474d9eb 100644 --- a/qtquick/qml/main.qml +++ b/qtquick/qml/main.qml @@ -16,13 +16,23 @@ ApplicationWindow { height: 600 visible: true property int _oldDepth: 0 - property alias playbackManager: playbackManager + property alias playbackManager: _playbackManager J.PlaybackManager { - id: playbackManager + id: _playbackManager apiClient: ApiClient } + J.PlatformMediaControl { + playbackManager: appWindow.playbackManager + canQuit: true + onQuitRequested: appWindow.close() + desktopFile: "sailfin" + playerName: "Sailfin" + canRaise: true + onRaiseRequested: appWindow.raise() + } + background: Background { id: background anchors.fill: parent @@ -77,6 +87,7 @@ ApplicationWindow { anchors.verticalCenter: parent.verticalCenter text: "Previous" onClicked: playbackManager.previous(); + enabled: playbackManager.hasPrevious } Button { readonly property bool _playing: playbackManager.playbackState === MediaPlayer.PlayingState; @@ -88,6 +99,7 @@ ApplicationWindow { anchors.verticalCenter: parent.verticalCenter text: "Next" onClicked: playbackManager.next(); + enabled: playbackManager.hasNext } } diff --git a/sailfish/qml/harbour-sailfin.qml b/sailfish/qml/harbour-sailfin.qml index e3091aa..73c0995 100644 --- a/sailfish/qml/harbour-sailfin.qml +++ b/sailfish/qml/harbour-sailfin.qml @@ -51,6 +51,15 @@ ApplicationWindow { supportedCommands: [GeneralCommandType.Play, GeneralCommandType.DisplayContent, GeneralCommandType.DisplayMessage] } + PlatformMediaControl { + playbackManager: appWindow.playbackManager + canQuit: fasle + desktopFile: "harbour-sailfin" + playerName: "Sailfin" + canRaise: true + onRaiseRequested: appWindow.raise() + } + initialPage: Component { MainPage { Connections {