From 84edc142ab1ca3d101d9a1a210ec74d65eed2e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sun, 16 Nov 2025 13:16:58 +0100 Subject: [PATCH 01/12] clapper: Prefer media scanner enhancer over legacy discoverer feature --- src/bin/clapper-app/clapper-app-window.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/bin/clapper-app/clapper-app-window.c b/src/bin/clapper-app/clapper-app-window.c index 04145119..f8fa4350 100644 --- a/src/bin/clapper-app/clapper-app-window.c +++ b/src/bin/clapper-app/clapper-app-window.c @@ -1292,9 +1292,13 @@ clapper_app_window_constructed (GObject *object) #endif #if CLAPPER_HAVE_DISCOVERER - feature = CLAPPER_FEATURE (clapper_discoverer_new ()); - clapper_player_add_feature (player, feature); - gst_object_unref (feature); + if ((proxy = clapper_enhancer_proxy_list_get_proxy_by_module (proxies, "clapper-media-scanner"))) { + gst_object_unref (proxy); + } else { + feature = CLAPPER_FEATURE (clapper_discoverer_new ()); + clapper_player_add_feature (player, feature); + gst_object_unref (feature); + } #endif /* FIXME: Allow setting sink/filter elements from prefs window From 1ae7d8deaddee39a7b1d6082f4c80bfd2e2cd5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sun, 16 Nov 2025 13:25:59 +0100 Subject: [PATCH 02/12] clapper: Deprecate Discoverer feature It was ported to Clapper Enhancers repo as a plugin and applications using old one should slowy move towards using new implementation instead. --- src/lib/clapper/clapper-enums.h | 2 ++ .../clapper/features/discoverer/clapper-discoverer.c | 10 ++++++++++ .../clapper/features/discoverer/clapper-discoverer.h | 8 ++++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/lib/clapper/clapper-enums.h b/src/lib/clapper/clapper-enums.h index 6f398bbb..818108bc 100644 --- a/src/lib/clapper/clapper-enums.h +++ b/src/lib/clapper/clapper-enums.h @@ -119,6 +119,8 @@ typedef enum * @CLAPPER_DISCOVERER_DISCOVERY_NONCURRENT: Only run discovery on an item if it is not a currently selected item in [class@Clapper.Queue]. * This mode is optimal when application always plays (or at least goes into paused) after selecting item from queue. * It will skip discovery of such items since they will be discovered by [class@Clapper.Player] anyway. + * + * Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead. */ typedef enum { diff --git a/src/lib/clapper/features/discoverer/clapper-discoverer.c b/src/lib/clapper/features/discoverer/clapper-discoverer.c index c9497434..19881894 100644 --- a/src/lib/clapper/features/discoverer/clapper-discoverer.c +++ b/src/lib/clapper/features/discoverer/clapper-discoverer.c @@ -36,6 +36,8 @@ * * Use [const@Clapper.HAVE_DISCOVERER] macro to check if Clapper API * was compiled with this feature. + * + * Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead. */ #include @@ -369,6 +371,8 @@ clapper_discoverer_unprepare (ClapperFeature *feature) * Creates a new #ClapperDiscoverer instance. * * Returns: (transfer full): a new #ClapperDiscoverer instance. + * + * Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead. */ ClapperDiscoverer * clapper_discoverer_new (void) @@ -385,6 +389,8 @@ clapper_discoverer_new (void) * @mode: a #ClapperDiscovererDiscoveryMode * * Set the [enum@Clapper.DiscovererDiscoveryMode] of @discoverer. + * + * Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead. */ void clapper_discoverer_set_discovery_mode (ClapperDiscoverer *self, ClapperDiscovererDiscoveryMode mode) @@ -409,6 +415,8 @@ clapper_discoverer_set_discovery_mode (ClapperDiscoverer *self, ClapperDiscovere * Get the [enum@Clapper.DiscovererDiscoveryMode] of @discoverer. * * Returns: a currently set #ClapperDiscovererDiscoveryMode. + * + * Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead. */ ClapperDiscovererDiscoveryMode clapper_discoverer_get_discovery_mode (ClapperDiscoverer *self) @@ -498,6 +506,8 @@ clapper_discoverer_class_init (ClapperDiscovererClass *klass) * ClapperDiscoverer:discovery-mode: * * Discoverer discovery mode. + * + * Deprecated: 0.10: Use Media Scanner from `clapper-enhancers` repo instead. */ param_specs[PROP_DISCOVERY_MODE] = g_param_spec_enum ("discovery-mode", NULL, NULL, CLAPPER_TYPE_DISCOVERER_DISCOVERY_MODE, DEFAULT_DISCOVERY_MODE, diff --git a/src/lib/clapper/features/discoverer/clapper-discoverer.h b/src/lib/clapper/features/discoverer/clapper-discoverer.h index f3bbdcd7..2055c9e5 100644 --- a/src/lib/clapper/features/discoverer/clapper-discoverer.h +++ b/src/lib/clapper/features/discoverer/clapper-discoverer.h @@ -33,16 +33,16 @@ G_BEGIN_DECLS #define CLAPPER_TYPE_DISCOVERER (clapper_discoverer_get_type()) #define CLAPPER_DISCOVERER_CAST(obj) ((ClapperDiscoverer *)(obj)) -CLAPPER_API +CLAPPER_DEPRECATED G_DECLARE_FINAL_TYPE (ClapperDiscoverer, clapper_discoverer, CLAPPER, DISCOVERER, ClapperFeature) -CLAPPER_API +CLAPPER_DEPRECATED ClapperDiscoverer * clapper_discoverer_new (void); -CLAPPER_API +CLAPPER_DEPRECATED void clapper_discoverer_set_discovery_mode (ClapperDiscoverer *discoverer, ClapperDiscovererDiscoveryMode mode); -CLAPPER_API +CLAPPER_DEPRECATED ClapperDiscovererDiscoveryMode clapper_discoverer_get_discovery_mode (ClapperDiscoverer *discoverer); G_END_DECLS From e85be687d6d6bd51fa5f54c338f9a34e9bae78e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sun, 16 Nov 2025 14:41:43 +0100 Subject: [PATCH 03/12] clapper: Add messaging between app, reactables and player In some cases, simple properties in reactable enhancers are not enough. This allows enhancers to do something on a signal from user (e.g. button/shortcut press). It can also be used to communicate with the player for things that were already done externally (like playlist parsing in Media Scanner enhancer). --- src/lib/clapper/clapper-enums.h | 15 ++ src/lib/clapper/clapper-playbin-bus-private.h | 2 + src/lib/clapper/clapper-playbin-bus.c | 171 +++++++++++------- src/lib/clapper/clapper-player.c | 50 +++++ src/lib/clapper/clapper-player.h | 3 + src/lib/clapper/clapper-reactable.h | 11 ++ .../clapper-reactables-manager-private.h | 5 + src/lib/clapper/clapper-reactables-manager.c | 36 ++++ 8 files changed, 228 insertions(+), 65 deletions(-) diff --git a/src/lib/clapper/clapper-enums.h b/src/lib/clapper/clapper-enums.h index 818108bc..58ea281f 100644 --- a/src/lib/clapper/clapper-enums.h +++ b/src/lib/clapper/clapper-enums.h @@ -54,6 +54,21 @@ typedef enum CLAPPER_PLAYER_SEEK_METHOD_FAST, } ClapperPlayerSeekMethod; +/** + * ClapperPlayerMessageDestination: + * @CLAPPER_PLAYER_MESSAGE_DESTINATION_PLAYER: Messaging from application or reactable enhancers to the player itself. + * @CLAPPER_PLAYER_MESSAGE_DESTINATION_REACTABLES: Messaging from application to the reactable enhancers. + * @CLAPPER_PLAYER_MESSAGE_DESTINATION_APPLICATION: Messaging from reactable enhancers to the application. + * + * Since: 0.10 + */ +typedef enum +{ + CLAPPER_PLAYER_MESSAGE_DESTINATION_PLAYER = 0, + CLAPPER_PLAYER_MESSAGE_DESTINATION_REACTABLES, + CLAPPER_PLAYER_MESSAGE_DESTINATION_APPLICATION, +} ClapperPlayerMessageDestination; + /** * ClapperQueueProgressionMode: * @CLAPPER_QUEUE_PROGRESSION_NONE: Queue will not change current item after playback finishes. diff --git a/src/lib/clapper/clapper-playbin-bus-private.h b/src/lib/clapper/clapper-playbin-bus-private.h index 2e858c5b..b06bed1f 100644 --- a/src/lib/clapper/clapper-playbin-bus-private.h +++ b/src/lib/clapper/clapper-playbin-bus-private.h @@ -49,4 +49,6 @@ void clapper_playbin_bus_post_current_item_change (GstBus *bus, ClapperMediaItem void clapper_playbin_bus_post_item_suburi_change (GstBus *bus, ClapperMediaItem *item); +void clapper_playbin_bus_post_user_message (GstBus *bus, GstMessage *msg); + G_END_DECLS diff --git a/src/lib/clapper/clapper-playbin-bus.c b/src/lib/clapper/clapper-playbin-bus.c index 706d080b..798ceee9 100644 --- a/src/lib/clapper/clapper-playbin-bus.c +++ b/src/lib/clapper/clapper-playbin-bus.c @@ -42,7 +42,8 @@ enum CLAPPER_PLAYBIN_BUS_STRUCTURE_RATE_CHANGE, CLAPPER_PLAYBIN_BUS_STRUCTURE_STREAM_CHANGE, CLAPPER_PLAYBIN_BUS_STRUCTURE_CURRENT_ITEM_CHANGE, - CLAPPER_PLAYBIN_BUS_STRUCTURE_ITEM_SUBURI_CHANGE + CLAPPER_PLAYBIN_BUS_STRUCTURE_ITEM_SUBURI_CHANGE, + CLAPPER_PLAYBIN_BUS_STRUCTURE_USER_MESSAGE }; static ClapperBusQuark _structure_quarks[] = { @@ -54,6 +55,7 @@ static ClapperBusQuark _structure_quarks[] = { {"stream-change", 0}, {"current-item-change", 0}, {"item-suburi-change", 0}, + {"user-message", 0}, {NULL, 0} }; @@ -793,6 +795,106 @@ _handle_stream_change_msg (GstMessage *msg, } } +void +clapper_playbin_bus_post_user_message (GstBus *bus, GstMessage *msg) +{ + GstStructure *structure = gst_structure_new_id_empty (_STRUCTURE_QUARK (USER_MESSAGE)); + GValue value = G_VALUE_INIT; + + g_value_init (&value, GST_TYPE_MESSAGE); + g_value_take_boxed (&value, msg); + + gst_structure_id_take_value (structure, _FIELD_QUARK (VALUE), &value); + + gst_bus_post (bus, gst_message_new_application (NULL, structure)); +} + +static inline void +_on_playlist_parsed_msg (GstMessage *msg, ClapperPlayer *player) +{ + ClapperMediaItem *playlist_item = NULL; + GListStore *playlist = NULL; + const GstStructure *structure = gst_message_get_structure (msg); + guint n_items; + + /* If message contains item, use that. + * Otherwise assume pending item was parsed. */ + if (gst_structure_has_field (structure, "item")) { + gst_structure_get (structure, + "item", CLAPPER_TYPE_MEDIA_ITEM, &playlist_item, NULL); + } else { + GST_OBJECT_LOCK (player); + + /* Playlist is always parsed before playback starts */ + if (player->pending_item) + playlist_item = gst_object_ref (player->pending_item); + + GST_OBJECT_UNLOCK (player); + } + + if (G_UNLIKELY (playlist_item == NULL)) { + GST_WARNING_OBJECT (player, "Playlist parsed without media item set"); + return; + } + + GST_INFO_OBJECT (player, "Received parsed playlist of %" GST_PTR_FORMAT + "(%s)", playlist_item, clapper_media_item_get_uri (playlist_item)); + + gst_structure_get (structure, + "playlist", G_TYPE_LIST_STORE, &playlist, NULL); + + n_items = g_list_model_get_n_items (G_LIST_MODEL (playlist)); + + if (G_LIKELY (n_items > 0)) { + ClapperMediaItem *active_item = g_list_model_get_item (G_LIST_MODEL (playlist), 0); + gboolean updated; + + /* Update redirect URI (must be done from player thread) */ + updated = clapper_media_item_update_from_item (playlist_item, active_item, player); + gst_object_unref (active_item); + + if (!updated) { + GstMessage *msg; + GError *error; + + error = g_error_new (GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_FAILED, + "Detected infinite redirection in playlist"); + msg = gst_message_new_error (GST_OBJECT (player), error, NULL); + + _handle_error_msg (msg, player); + + g_error_free (error); + gst_message_unref (msg); + } else if (n_items > 1) { + /* Forward to append remaining items (must be done from main thread) */ + clapper_app_bus_post_insert_playlist (player->app_bus, + GST_OBJECT_CAST (player), + GST_OBJECT_CAST (playlist_item), + G_OBJECT (playlist)); + } + } + + gst_object_unref (playlist_item); + g_object_unref (playlist); +} + +static inline void +_handle_user_message_msg (GstMessage *msg, const GstStructure *structure, ClapperPlayer *player) +{ + GstMessage *user_message = NULL; + + gst_structure_id_get (structure, + _FIELD_QUARK (VALUE), GST_TYPE_MESSAGE, &user_message, + NULL); + + GST_DEBUG_OBJECT (player, "Received user message: %" GST_PTR_FORMAT, user_message); + + if (gst_message_has_name (user_message, "ClapperPlaylistParsed")) + _on_playlist_parsed_msg (user_message, player); + + gst_message_unref (user_message); +} + static inline void _handle_app_msg (GstMessage *msg, ClapperPlayer *player) { @@ -813,6 +915,8 @@ _handle_app_msg (GstMessage *msg, ClapperPlayer *player) _handle_current_item_change_msg (msg, structure, player); else if (quark == _STRUCTURE_QUARK (ITEM_SUBURI_CHANGE)) _handle_item_suburi_change_msg (msg, structure, player); + else if (quark == _STRUCTURE_QUARK (USER_MESSAGE)) + _handle_user_message_msg (msg, structure, player); } static inline void @@ -832,70 +936,7 @@ _handle_element_msg (GstMessage *msg, ClapperPlayer *player) g_free (name); g_free (details); } else if (gst_message_has_name (msg, "ClapperPlaylistParsed")) { - ClapperMediaItem *playlist_item = NULL; - GListStore *playlist = NULL; - const GstStructure *structure = gst_message_get_structure (msg); - guint n_items; - - /* If message contains item, use that. - * Otherwise assume pending item was parsed. */ - if (gst_structure_has_field (structure, "item")) { - gst_structure_get (structure, - "item", CLAPPER_TYPE_MEDIA_ITEM, &playlist_item, NULL); - } else { - GST_OBJECT_LOCK (player); - - /* Playlist is always parsed before playback starts */ - if (player->pending_item) - playlist_item = gst_object_ref (player->pending_item); - - GST_OBJECT_UNLOCK (player); - } - - if (G_UNLIKELY (playlist_item == NULL)) { - GST_WARNING_OBJECT (player, "Playlist parsed without media item set"); - return; - } - - GST_INFO_OBJECT (player, "Received parsed playlist of %" GST_PTR_FORMAT - "(%s)", playlist_item, clapper_media_item_get_uri (playlist_item)); - - gst_structure_get (structure, - "playlist", G_TYPE_LIST_STORE, &playlist, NULL); - - n_items = g_list_model_get_n_items (G_LIST_MODEL (playlist)); - - if (G_LIKELY (n_items > 0)) { - ClapperMediaItem *active_item = g_list_model_get_item (G_LIST_MODEL (playlist), 0); - gboolean updated; - - /* Update redirect URI (must be done from player thread) */ - updated = clapper_media_item_update_from_item (playlist_item, active_item, player); - gst_object_unref (active_item); - - if (!updated) { - GstMessage *msg; - GError *error; - - error = g_error_new (GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_FAILED, - "Detected infinite redirection in playlist"); - msg = gst_message_new_error (GST_OBJECT (player), error, NULL); - - _handle_error_msg (msg, player); - - g_error_free (error); - gst_message_unref (msg); - } else if (n_items > 1) { - /* Forward to append remaining items (must be done from main thread) */ - clapper_app_bus_post_insert_playlist (player->app_bus, - GST_OBJECT_CAST (player), - GST_OBJECT_CAST (playlist_item), - G_OBJECT (playlist)); - } - } - - gst_object_unref (playlist_item); - g_object_unref (playlist); + _on_playlist_parsed_msg (msg, player); } else if (gst_message_has_name (msg, "GstCacheDownloadComplete")) { ClapperMediaItem *downloaded_item = NULL; const GstStructure *structure; diff --git a/src/lib/clapper/clapper-player.c b/src/lib/clapper/clapper-player.c index 39dee096..89602e7d 100644 --- a/src/lib/clapper/clapper-player.c +++ b/src/lib/clapper/clapper-player.c @@ -2245,6 +2245,56 @@ clapper_player_make_pipeline_graph (ClapperPlayer *self, GstDebugGraphDetails de return gst_debug_bin_to_dot_data (GST_BIN (self->playbin), details); } +/** + * clapper_player_post_message: + * @player: a #ClapperPlayer + * @msg: (transfer full): a #GstMessage + * @destination: a #ClapperPlayerMessageDestination + * + * Allows sending custom messages to the desired @destination. + * + * This functionality can be used for communication with enhancers implementing + * [iface@Clapper.Reactable] interface. Useful for applications to send custom messages + * to enhacers that can react to them and/or for enhancers development to send events + * from them to the applications. It can also be used for sending specific messages + * from application or enhancers to the player itself. + * + * Messages send to the application can be received by connecting a + * [signal@Clapper.Player::message] signal handler. Inspection of message source + * object can be done to determine who send given message. + * + * Since: 0.10 + */ +void +clapper_player_post_message (ClapperPlayer *self, GstMessage *msg, + ClapperPlayerMessageDestination destination) +{ + g_return_if_fail (CLAPPER_IS_PLAYER (self)); + g_return_if_fail (GST_IS_MESSAGE (msg)); + + switch (destination) { + case CLAPPER_PLAYER_MESSAGE_DESTINATION_PLAYER: + clapper_playbin_bus_post_user_message (self->bus, msg); + return; // Message taken + case CLAPPER_PLAYER_MESSAGE_DESTINATION_REACTABLES: + if (self->reactables_manager) { + clapper_reactables_manager_post_message (self->reactables_manager, msg); + return; // Message taken + } + break; + case CLAPPER_PLAYER_MESSAGE_DESTINATION_APPLICATION: + clapper_app_bus_post_message_signal (self->app_bus, + GST_OBJECT_CAST (self), signals[SIGNAL_MESSAGE], msg); + break; // Above function does not take message (unref needed) + default: + g_assert_not_reached (); + break; + } + + /* Unref when not taken */ + gst_message_unref (msg); +} + static void clapper_player_thread_start (ClapperThreadedObject *threaded_object) { diff --git a/src/lib/clapper/clapper-player.h b/src/lib/clapper/clapper-player.h index 0e69b566..45fc31d2 100644 --- a/src/lib/clapper/clapper-player.h +++ b/src/lib/clapper/clapper-player.h @@ -210,4 +210,7 @@ void clapper_player_add_feature (ClapperPlayer *player, ClapperFeature *feature) CLAPPER_API gchar * clapper_player_make_pipeline_graph (ClapperPlayer *player, GstDebugGraphDetails details); +CLAPPER_API +void clapper_player_post_message (ClapperPlayer *player, GstMessage *msg, ClapperPlayerMessageDestination destination); + G_END_DECLS diff --git a/src/lib/clapper/clapper-reactable.h b/src/lib/clapper/clapper-reactable.h index 8a376465..5b321889 100644 --- a/src/lib/clapper/clapper-reactable.h +++ b/src/lib/clapper/clapper-reactable.h @@ -206,6 +206,17 @@ struct _ClapperReactableInterface */ void (* queue_progression_changed) (ClapperReactable *reactable, ClapperQueueProgressionMode mode); + /** + * ClapperReactableInterface::message_received: + * @reactable: a #ClapperReactable + * @msg: a #GstMessage + * + * Custom message from user was received on reactables bus. + * + * Since: 0.10 + */ + void (* message_received) (ClapperReactable *reactable, GstMessage *msg); + /*< private >*/ gpointer padding[8]; }; diff --git a/src/lib/clapper/clapper-reactables-manager-private.h b/src/lib/clapper/clapper-reactables-manager-private.h index d9e27586..cf0d837e 100644 --- a/src/lib/clapper/clapper-reactables-manager-private.h +++ b/src/lib/clapper/clapper-reactables-manager-private.h @@ -18,6 +18,8 @@ #pragma once +#include + #include "clapper-enums.h" #include "clapper-threaded-object.h" #include "clapper-enhancer-proxy.h" @@ -36,6 +38,9 @@ void clapper_reactables_manager_initialize (void); G_GNUC_INTERNAL ClapperReactablesManager * clapper_reactables_manager_new (void); +G_GNUC_INTERNAL +void clapper_reactables_manager_post_message (ClapperReactablesManager *manager, GstMessage *msg); + G_GNUC_INTERNAL void clapper_reactables_manager_trigger_configure_take_config (ClapperReactablesManager *manager, ClapperEnhancerProxy *proxy, GstStructure *config); diff --git a/src/lib/clapper/clapper-reactables-manager.c b/src/lib/clapper/clapper-reactables-manager.c index faccfb66..8b08c297 100644 --- a/src/lib/clapper/clapper-reactables-manager.c +++ b/src/lib/clapper/clapper-reactables-manager.c @@ -78,6 +78,7 @@ enum { CLAPPER_REACTABLES_MANAGER_QUARK_CONFIGURE = 0, CLAPPER_REACTABLES_MANAGER_QUARK_EVENT, + CLAPPER_REACTABLES_MANAGER_QUARK_USER_MESSAGE, CLAPPER_REACTABLES_MANAGER_QUARK_VALUE, CLAPPER_REACTABLES_MANAGER_QUARK_EXTRA_VALUE }; @@ -85,6 +86,7 @@ enum static ClapperBusQuark _quarks[] = { {"configure", 0}, {"event", 0}, + {"user-message", 0}, {"value", 0}, {"extra-value", 0}, {NULL, 0} @@ -308,6 +310,23 @@ clapper_reactables_manager_handle_event (ClapperReactablesManager *self, const G } } +static inline void +clapper_reactables_manager_handle_user_message (ClapperReactablesManager *self, const GstStructure *structure) +{ + const GValue *value = gst_structure_id_get_value (structure, _QUARK (VALUE)); + guint i; + + for (i = 0; i < self->array->len; ++i) { + ClapperReactableManagerData *data = g_ptr_array_index (self->array, i); + ClapperReactableInterface *reactable_iface = CLAPPER_REACTABLE_GET_IFACE (data->reactable); + + if (reactable_iface->message_received) { + reactable_iface->message_received (data->reactable, + GST_MESSAGE_CAST (g_value_get_boxed (value))); + } + } +} + static gboolean _bus_message_func (GstBus *bus, GstMessage *msg, gpointer user_data G_GNUC_UNUSED) { @@ -324,6 +343,8 @@ _bus_message_func (GstBus *bus, GstMessage *msg, gpointer user_data G_GNUC_UNUSE clapper_reactables_manager_handle_event (self, structure); } else if (quark == _QUARK (CONFIGURE)) { clapper_reactables_manager_handle_configure (self, structure); + } else if (quark == _QUARK (USER_MESSAGE)) { + clapper_reactables_manager_handle_user_message (self, structure); } else { GST_ERROR_OBJECT (self, "Received invalid quark on reactables bus!"); } @@ -365,6 +386,21 @@ clapper_reactables_manager_new (void) return reactables_manager; } +void +clapper_reactables_manager_post_message (ClapperReactablesManager *self, GstMessage *msg) +{ + GstStructure *structure = gst_structure_new_id_empty (_QUARK (USER_MESSAGE)); + GValue value = G_VALUE_INIT; + + g_value_init (&value, GST_TYPE_MESSAGE); + g_value_take_boxed (&value, msg); + + gst_structure_id_take_value (structure, _QUARK (VALUE), &value); + + gst_bus_post (self->bus, gst_message_new_application ( + GST_OBJECT_CAST (self), structure)); +} + void clapper_reactables_manager_trigger_configure_take_config (ClapperReactablesManager *self, ClapperEnhancerProxy *proxy, GstStructure *config) From a9cdbf8626963587abf1444197ae85264dae1fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sun, 16 Nov 2025 14:49:05 +0100 Subject: [PATCH 04/12] clapper: Mark item title as always parsed This should be used internally for whether title is still used from parsing, not if parsing was successful. Fixes an issue where in such cases correct title is not updated later on. --- src/lib/clapper/clapper-media-item.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/clapper/clapper-media-item.c b/src/lib/clapper/clapper-media-item.c index ea89529b..12ca6209 100644 --- a/src/lib/clapper/clapper-media-item.c +++ b/src/lib/clapper/clapper-media-item.c @@ -814,7 +814,7 @@ clapper_media_item_constructed (GObject *object) self->uri = g_strdup ("file://"); self->title = clapper_utils_title_from_uri (self->uri); - self->title_is_parsed = (self->title != NULL); + self->title_is_parsed = TRUE; G_OBJECT_CLASS (parent_class)->constructed (object); } From 10bb4365a03a46057719ba1469389e9778319f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sun, 16 Nov 2025 14:55:02 +0100 Subject: [PATCH 05/12] clapper: Use correct types for enums/flags in playbin bus We send read them as simple integers, thus we should send them as such instead of using GObject Flag or Enum types. --- src/lib/clapper/clapper-playbin-bus.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/clapper/clapper-playbin-bus.c b/src/lib/clapper/clapper-playbin-bus.c index 798ceee9..347beb09 100644 --- a/src/lib/clapper/clapper-playbin-bus.c +++ b/src/lib/clapper/clapper-playbin-bus.c @@ -327,7 +327,7 @@ clapper_playbin_bus_post_set_play_flag (GstBus *bus, ClapperPlayerPlayFlags flag, gboolean enabled) { GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (SET_PLAY_FLAG), - _FIELD_QUARK (FLAG), G_TYPE_FLAGS, flag, + _FIELD_QUARK (FLAG), G_TYPE_UINT, flag, _FIELD_QUARK (VALUE), G_TYPE_BOOLEAN, enabled, NULL); gst_bus_post (bus, gst_message_new_application (NULL, structure)); @@ -338,10 +338,10 @@ _handle_set_play_flag_msg (GstMessage *msg, const GstStructure *structure, Clapp { ClapperPlayerPlayFlags flag = 0; gboolean enabled, enable = FALSE; - gint flags = 0; + guint flags = 0; gst_structure_id_get (structure, - _FIELD_QUARK (FLAG), G_TYPE_FLAGS, &flag, + _FIELD_QUARK (FLAG), G_TYPE_UINT, &flag, _FIELD_QUARK (VALUE), G_TYPE_BOOLEAN, &enable, NULL); @@ -404,7 +404,7 @@ clapper_playbin_bus_post_seek (GstBus *bus, gdouble position, ClapperPlayerSeekM { GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (SEEK), _FIELD_QUARK (POSITION), G_TYPE_INT64, (gint64) (position * GST_SECOND), - _FIELD_QUARK (SEEK_METHOD), G_TYPE_ENUM, seek_method, + _FIELD_QUARK (SEEK_METHOD), G_TYPE_INT, seek_method, NULL); gst_bus_post (bus, gst_message_new_application (NULL, structure)); } @@ -424,7 +424,7 @@ _handle_seek_msg (GstMessage *msg, const GstStructure *structure, ClapperPlayer gst_structure_id_get (structure, _FIELD_QUARK (POSITION), G_TYPE_INT64, &position, - _FIELD_QUARK (SEEK_METHOD), G_TYPE_ENUM, &seek_method, + _FIELD_QUARK (SEEK_METHOD), G_TYPE_INT, &seek_method, NULL); /* If we are starting playback, do a seek after preroll */ @@ -648,7 +648,7 @@ clapper_playbin_bus_post_current_item_change (GstBus *bus, ClapperMediaItem *cur { GstStructure *structure = gst_structure_new_id (_STRUCTURE_QUARK (CURRENT_ITEM_CHANGE), _FIELD_QUARK (MEDIA_ITEM), CLAPPER_TYPE_MEDIA_ITEM, current_item, - _FIELD_QUARK (ITEM_CHANGE_MODE), G_TYPE_ENUM, mode, + _FIELD_QUARK (ITEM_CHANGE_MODE), G_TYPE_INT, mode, NULL); gst_bus_post (bus, gst_message_new_application (NULL, structure)); } @@ -661,7 +661,7 @@ _handle_current_item_change_msg (GstMessage *msg, const GstStructure *structure, gst_structure_id_get (structure, _FIELD_QUARK (MEDIA_ITEM), CLAPPER_TYPE_MEDIA_ITEM, ¤t_item, - _FIELD_QUARK (ITEM_CHANGE_MODE), G_TYPE_ENUM, &mode, + _FIELD_QUARK (ITEM_CHANGE_MODE), G_TYPE_INT, &mode, NULL); player->pending_position = 0; // We store pending position for played item, so reset From 9e955f4e5f1956245d8a4a147c6e4c194a07f1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Fri, 21 Nov 2025 18:04:15 +0100 Subject: [PATCH 06/12] clapper: Prevent concurrent cache cleanups We do not want to create situations where multiple threads are running cache cleanup at once (since they all think last one was run long ago). This can be prevented with a combination of global mutex and a "trylock", so we can simply avoid both mutex locking and starting another cleanup. --- src/lib/clapper/gst/clapper-enhancer-director.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/clapper/gst/clapper-enhancer-director.c b/src/lib/clapper/gst/clapper-enhancer-director.c index c8f3ac7d..1f71cd31 100644 --- a/src/lib/clapper/gst/clapper-enhancer-director.c +++ b/src/lib/clapper/gst/clapper-enhancer-director.c @@ -60,6 +60,8 @@ typedef struct GError **error; } ClapperEnhancerDirectorData; +static GMutex cleanup_lock; + static gpointer clapper_enhancer_director_extract_in_thread (ClapperEnhancerDirectorData *data) { @@ -356,6 +358,11 @@ _cache_cleanup_func (ClapperEnhancerDirector *self) const gchar *data; gint64 since_cleanup, epoch_now, epoch_last = 0; + if (!g_mutex_trylock (&cleanup_lock)) { + GST_LOG_OBJECT (self, "Cache cleanup is already running"); + return G_SOURCE_REMOVE; + } + date = g_date_time_new_now_utc (); epoch_now = g_date_time_to_unix (date); g_date_time_unref (date); @@ -418,6 +425,7 @@ _cache_cleanup_func (ClapperEnhancerDirector *self) CLAPPER_TIME_FORMAT " ago", CLAPPER_TIME_ARGS (since_cleanup)); } + g_mutex_unlock (&cleanup_lock); g_free (filename); return G_SOURCE_REMOVE; From 4acb3e38b327277ef5852f3529ff0ca5427164f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Fri, 21 Nov 2025 18:18:49 +0100 Subject: [PATCH 07/12] clapper: Post playlist message before setting next URI Fixes a problem where error message from unplayable URI content (e.g. non-existing file) arrives on pipeline bus before resolved playlist which contains that URI. With this change, a playlist can be resolved into individual items which are put into queue even if first item of it is unplayable. --- src/lib/clapper/gst/clapper-playlist-demux.c | 26 +++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/lib/clapper/gst/clapper-playlist-demux.c b/src/lib/clapper/gst/clapper-playlist-demux.c index fb123240..dc83b161 100644 --- a/src/lib/clapper/gst/clapper-playlist-demux.c +++ b/src/lib/clapper/gst/clapper-playlist-demux.c @@ -309,16 +309,32 @@ _filter_playlistables (ClapperPlaylistDemux *self, GstCaps *caps, ClapperEnhance static inline gboolean _handle_playlist (ClapperPlaylistDemux *self, GListStore *playlist, GCancellable *cancellable) { - ClapperMediaItem *item = g_list_model_get_item (G_LIST_MODEL (playlist), 0); + ClapperMediaItem *item; + GstStructure *structure; const gchar *uri; gboolean success; + if (g_cancellable_is_cancelled (cancellable)) { + GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ, + ("Playlist parsing was cancelled"), (NULL)); + return FALSE; + } + + item = g_list_model_get_item (G_LIST_MODEL (playlist), 0); + if (G_UNLIKELY (item == NULL)) { GST_ELEMENT_ERROR (self, RESOURCE, OPEN_READ, ("This playlist appears to be empty"), (NULL)); return FALSE; } + /* Post playlist before setting an URI, so it arrives + * before eventual error (e.g. non-existing file) */ + structure = gst_structure_new ("ClapperPlaylistParsed", + "playlist", G_TYPE_LIST_STORE, playlist, NULL); + gst_element_post_message (GST_ELEMENT_CAST (self), + gst_message_new_element (GST_OBJECT_CAST (self), structure)); + uri = clapper_media_item_get_uri (item); success = clapper_uri_base_demux_set_uri (CLAPPER_URI_BASE_DEMUX_CAST (self), uri, NULL); gst_object_unref (item); @@ -329,14 +345,6 @@ _handle_playlist (ClapperPlaylistDemux *self, GListStore *playlist, GCancellable return FALSE; } - if (!g_cancellable_is_cancelled (cancellable)) { - GstStructure *structure = gst_structure_new ("ClapperPlaylistParsed", - "playlist", G_TYPE_LIST_STORE, playlist, NULL); - - gst_element_post_message (GST_ELEMENT_CAST (self), - gst_message_new_element (GST_OBJECT_CAST (self), structure)); - } - return TRUE; } From 05b7314b735066dc4e007f12ed8b6b92f1b718ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sat, 22 Nov 2025 20:44:08 +0100 Subject: [PATCH 08/12] clapper: Reworked infinite nested playlists detection Instead of doing this inside player, logic was reworked and moved into playlist demuxer itself. It uses GstQueries to communicate with upstream elements. This way we can post an error from running pipeline which helps keep this logic in single place and have it shared with other pipelines (like the one used in Media Scanner Enhancer). As part of the rework, nested playlist limit was added and set to 10 redirects. Seems like a safe/optimal number and this simplifies stuff a lot. Instead of keeping a list of strings with previous redirected URIs, we can just store the one most recent in media item. --- src/lib/clapper/clapper-media-item-private.h | 2 +- src/lib/clapper/clapper-media-item.c | 45 ++++++++---- src/lib/clapper/clapper-playbin-bus.c | 33 ++++----- src/lib/clapper/gst/clapper-playlist-demux.c | 70 ++++++++++++++++++- .../gst/clapper-uri-base-demux-private.h | 2 + src/lib/clapper/gst/clapper-uri-base-demux.c | 15 ++++ 6 files changed, 132 insertions(+), 35 deletions(-) diff --git a/src/lib/clapper/clapper-media-item-private.h b/src/lib/clapper/clapper-media-item-private.h index d1666355..c48467ff 100644 --- a/src/lib/clapper/clapper-media-item-private.h +++ b/src/lib/clapper/clapper-media-item-private.h @@ -34,7 +34,7 @@ G_GNUC_INTERNAL void clapper_media_item_update_from_discoverer_info (ClapperMediaItem *self, GstDiscovererInfo *info); G_GNUC_INTERNAL -gboolean clapper_media_item_update_from_item (ClapperMediaItem *item, ClapperMediaItem *other_item, ClapperPlayer *player); +gboolean clapper_media_item_update_from_parsed_playlist (ClapperMediaItem *item, GListStore *playlist, GstObject *playlist_src, ClapperPlayer *player); G_GNUC_INTERNAL gboolean clapper_media_item_set_duration (ClapperMediaItem *item, gdouble duration, ClapperAppBus *app_bus); diff --git a/src/lib/clapper/clapper-media-item.c b/src/lib/clapper/clapper-media-item.c index 12ca6209..cffd2f55 100644 --- a/src/lib/clapper/clapper-media-item.c +++ b/src/lib/clapper/clapper-media-item.c @@ -54,7 +54,7 @@ struct _ClapperMediaItem /* Whether using title from URI */ gboolean title_is_parsed; - GSList *redirects; + gchar *redirect_uri; gchar *cache_uri; /* For shuffle */ @@ -203,8 +203,8 @@ clapper_media_item_get_id (ClapperMediaItem *self) } /* FIXME: 1.0: - * Consider change to be transfer-full and just return latest data from redirects - * list (alternatively expose redirect URI). This should make it possible to work + * Consider change to be transfer-full and just return latest redirect URI + * (alternatively expose redirect URI). This should make it possible to work * with enhancers that would benefit from knowledge about URI changes * (e.g "Recall" could read actual media instead of playlist file). */ @@ -681,26 +681,41 @@ clapper_media_item_update_from_discoverer_info (ClapperMediaItem *self, GstDisco /* XXX: Must be set from player thread */ static inline gboolean -clapper_media_item_set_redirect_uri (ClapperMediaItem *self, const gchar *redirect_uri) +clapper_media_item_set_redirect_uri (ClapperMediaItem *self, const gchar *redirect_uri, + GstObject *redirect_src) { - /* Check if we did not already redirect into that URI (prevent endless loop) */ - if (!redirect_uri || g_slist_find_custom (self->redirects, redirect_uri, (GCompareFunc) strcmp)) + /* Safety checks */ + if (G_UNLIKELY (!redirect_uri)) { + GST_ERROR_OBJECT (self, "Received redirect request without an URI set"); return FALSE; + } + if (G_UNLIKELY (!redirect_src)) { + GST_ERROR_OBJECT (self, "Received redirect request without source object set"); + return FALSE; + } - self->redirects = g_slist_prepend (self->redirects, g_strdup (redirect_uri)); - GST_DEBUG_OBJECT (self, "Set redirect URI: \"%s\"", (gchar *) self->redirects->data); + g_set_str (&self->redirect_uri, redirect_uri); + GST_DEBUG_OBJECT (self, "Set redirect URI: \"%s\", source: %s", + self->redirect_uri, G_OBJECT_TYPE_NAME (redirect_src)); return TRUE; } gboolean -clapper_media_item_update_from_item (ClapperMediaItem *self, ClapperMediaItem *other_item, - ClapperPlayer *player) +clapper_media_item_update_from_parsed_playlist (ClapperMediaItem *self, GListStore *playlist, + GstObject *playlist_src, ClapperPlayer *player) { + ClapperMediaItem *other_item; + const gchar *redirect_uri; gboolean title_changed = FALSE; - if (!clapper_media_item_set_redirect_uri (self, clapper_media_item_get_uri (other_item))) + other_item = g_list_model_get_item (G_LIST_MODEL (playlist), 0); + redirect_uri = clapper_media_item_get_uri (other_item); + + if (!clapper_media_item_set_redirect_uri (self, redirect_uri, playlist_src)) { + gst_object_unref (other_item); return FALSE; + } GST_OBJECT_LOCK (other_item); @@ -731,6 +746,8 @@ clapper_media_item_update_from_item (ClapperMediaItem *self, ClapperMediaItem *o clapper_features_manager_trigger_item_updated (features_manager, self); } + gst_object_unref (other_item); + return TRUE; } @@ -768,8 +785,8 @@ clapper_media_item_get_playback_uri (ClapperMediaItem *self) clapper_media_item_set_cache_location (self, NULL); } - if (self->redirects) - return self->redirects->data; + if (self->redirect_uri) + return self->redirect_uri; return self->uri; } @@ -835,7 +852,7 @@ clapper_media_item_finalize (GObject *object) gst_object_unparent (GST_OBJECT_CAST (self->timeline)); gst_object_unref (self->timeline); - g_slist_free_full (self->redirects, g_free); + g_free (self->redirect_uri); g_free (self->cache_uri); G_OBJECT_CLASS (parent_class)->finalize (object); diff --git a/src/lib/clapper/clapper-playbin-bus.c b/src/lib/clapper/clapper-playbin-bus.c index 347beb09..9e5476e5 100644 --- a/src/lib/clapper/clapper-playbin-bus.c +++ b/src/lib/clapper/clapper-playbin-bus.c @@ -29,6 +29,7 @@ #include "clapper-stream-private.h" #include "clapper-stream-list-private.h" #include "gst/clapper-extractable-src-private.h" +#include "gst/clapper-playlist-demux-private.h" #define GST_CAT_DEFAULT clapper_playbin_bus_debug GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); @@ -812,20 +813,28 @@ clapper_playbin_bus_post_user_message (GstBus *bus, GstMessage *msg) static inline void _on_playlist_parsed_msg (GstMessage *msg, ClapperPlayer *player) { + GstObject *src = GST_MESSAGE_SRC (msg); ClapperMediaItem *playlist_item = NULL; GListStore *playlist = NULL; - const GstStructure *structure = gst_message_get_structure (msg); + const GstStructure *structure; guint n_items; + if (G_UNLIKELY (!src)) { + GST_WARNING_OBJECT (player, "Ignoring playlist parsed message without a source"); + return; + } + + structure = gst_message_get_structure (msg); + /* If message contains item, use that. * Otherwise assume pending item was parsed. */ if (gst_structure_has_field (structure, "item")) { gst_structure_get (structure, "item", CLAPPER_TYPE_MEDIA_ITEM, &playlist_item, NULL); - } else { + } else if (CLAPPER_IS_PLAYLIST_DEMUX (src)) { GST_OBJECT_LOCK (player); - /* Playlist is always parsed before playback starts */ + /* Playlist from demuxer is always parsed before playback starts */ if (player->pending_item) playlist_item = gst_object_ref (player->pending_item); @@ -846,26 +855,12 @@ _on_playlist_parsed_msg (GstMessage *msg, ClapperPlayer *player) n_items = g_list_model_get_n_items (G_LIST_MODEL (playlist)); if (G_LIKELY (n_items > 0)) { - ClapperMediaItem *active_item = g_list_model_get_item (G_LIST_MODEL (playlist), 0); gboolean updated; /* Update redirect URI (must be done from player thread) */ - updated = clapper_media_item_update_from_item (playlist_item, active_item, player); - gst_object_unref (active_item); + updated = clapper_media_item_update_from_parsed_playlist (playlist_item, playlist, src, player); - if (!updated) { - GstMessage *msg; - GError *error; - - error = g_error_new (GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_FAILED, - "Detected infinite redirection in playlist"); - msg = gst_message_new_error (GST_OBJECT (player), error, NULL); - - _handle_error_msg (msg, player); - - g_error_free (error); - gst_message_unref (msg); - } else if (n_items > 1) { + if (updated && n_items > 1) { /* Forward to append remaining items (must be done from main thread) */ clapper_app_bus_post_insert_playlist (player->app_bus, GST_OBJECT_CAST (player), diff --git a/src/lib/clapper/gst/clapper-playlist-demux.c b/src/lib/clapper/gst/clapper-playlist-demux.c index dc83b161..0c7afac4 100644 --- a/src/lib/clapper/gst/clapper-playlist-demux.c +++ b/src/lib/clapper/gst/clapper-playlist-demux.c @@ -30,6 +30,10 @@ #define URI_LIST_MEDIA_TYPE "text/uri-list" #define DATA_CHUNK_SIZE 4096 +#define NTH_REDIRECT_STRUCTURE_NAME "ClapperQueryNthRedirect" +#define NTH_REDIRECT_FIELD "nth-redirect" +#define MAX_REDIRECTS 10 + #define GST_CAT_DEFAULT clapper_playlist_demux_debug GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); @@ -348,6 +352,50 @@ _handle_playlist (ClapperPlaylistDemux *self, GListStore *playlist, GCancellable return TRUE; } +static void +_query_parse_nth_redirect (GstQuery *query, guint *nth_redirect) +{ + const GstStructure *structure = gst_query_get_structure (query); + *nth_redirect = g_value_get_uint (gst_structure_get_value (structure, NTH_REDIRECT_FIELD)); +} + +static void +_query_set_nth_redirect (GstQuery *query, guint nth_redirect) +{ + GstStructure *structure = gst_query_writable_structure (query); + gst_structure_set (structure, NTH_REDIRECT_FIELD, G_TYPE_UINT, nth_redirect, NULL); +} + +static gboolean +clapper_playlist_demux_handle_custom_query (ClapperUriBaseDemux *uri_bd, GstQuery *query) +{ + ClapperPlaylistDemux *self = CLAPPER_PLAYLIST_DEMUX_CAST (uri_bd); + const GstStructure *structure = gst_query_get_structure (query); + + if (gst_structure_has_name (structure, NTH_REDIRECT_STRUCTURE_NAME)) { + GstPad *sink_pad; + + GST_LOG_OBJECT (self, "Received custom query: " NTH_REDIRECT_STRUCTURE_NAME); + + sink_pad = gst_element_get_static_pad (GST_ELEMENT_CAST (self), "sink"); + gst_pad_peer_query (sink_pad, query); + gst_object_unref (sink_pad); + + if (G_LIKELY (gst_query_is_writable (query))) { + guint nth_redirect = 0; + + _query_parse_nth_redirect (query, &nth_redirect); + _query_set_nth_redirect (query, ++nth_redirect); + } else { + GST_ERROR_OBJECT (self, "Unwritable custom query: " NTH_REDIRECT_STRUCTURE_NAME); + } + + return TRUE; + } + + return FALSE; +} + static gboolean clapper_playlist_demux_process_buffer (ClapperUriBaseDemux *uri_bd, GstBuffer *buffer, GCancellable *cancellable) @@ -355,9 +403,11 @@ clapper_playlist_demux_process_buffer (ClapperUriBaseDemux *uri_bd, ClapperPlaylistDemux *self = CLAPPER_PLAYLIST_DEMUX_CAST (uri_bd); GstPad *sink_pad; GstQuery *query; + GstStructure *query_structure; GUri *uri = NULL; GListStore *playlist; GError *error = NULL; + guint nth_redirect = 0; gboolean handled; sink_pad = gst_element_get_static_pad (GST_ELEMENT_CAST (self), "sink"); @@ -375,11 +425,28 @@ clapper_playlist_demux_process_buffer (ClapperUriBaseDemux *uri_bd, } } + gst_query_unref (query); + + query_structure = gst_structure_new (NTH_REDIRECT_STRUCTURE_NAME, + NTH_REDIRECT_FIELD, G_TYPE_UINT, 0, NULL); + query = gst_query_new_custom (GST_QUERY_CUSTOM, query_structure); + + if (gst_pad_peer_query (sink_pad, query)) + _query_parse_nth_redirect (query, &nth_redirect); + + GST_DEBUG_OBJECT (self, "Current number of redirects: %u", nth_redirect); + gst_query_unref (query); gst_object_unref (sink_pad); if (G_UNLIKELY (uri == NULL)) { - GST_ERROR_OBJECT (self, "Could not query source URI"); + GST_ELEMENT_ERROR (self, RESOURCE, FAILED, + ("Could not query source URI"), (NULL)); + return FALSE; + } + if (G_UNLIKELY (nth_redirect > MAX_REDIRECTS)) { + GST_ELEMENT_ERROR (self, RESOURCE, FAILED, + ("Too many nested playlists"), (NULL)); return FALSE; } @@ -506,6 +573,7 @@ clapper_playlist_demux_class_init (ClapperPlaylistDemuxClass *klass) gobject_class->finalize = clapper_playlist_demux_finalize; clapperuribd_class->handle_caps = clapper_playlist_demux_handle_caps; + clapperuribd_class->handle_custom_query = clapper_playlist_demux_handle_custom_query; clapperuribd_class->process_buffer = clapper_playlist_demux_process_buffer; param_specs[PROP_ENHANCER_PROXIES] = g_param_spec_object ("enhancer-proxies", diff --git a/src/lib/clapper/gst/clapper-uri-base-demux-private.h b/src/lib/clapper/gst/clapper-uri-base-demux-private.h index 3237c419..169b91e3 100644 --- a/src/lib/clapper/gst/clapper-uri-base-demux-private.h +++ b/src/lib/clapper/gst/clapper-uri-base-demux-private.h @@ -41,6 +41,8 @@ struct _ClapperUriBaseDemuxClass void (* handle_caps) (ClapperUriBaseDemux *uri_bd, GstCaps *caps); void (* handle_custom_event) (ClapperUriBaseDemux *uri_bd, GstEvent *event); + + gboolean (* handle_custom_query) (ClapperUriBaseDemux *uri_bd, GstQuery *query); }; gboolean clapper_uri_base_demux_set_uri (ClapperUriBaseDemux *uri_bd, const gchar *uri, const gchar *blacklisted_el); diff --git a/src/lib/clapper/gst/clapper-uri-base-demux.c b/src/lib/clapper/gst/clapper-uri-base-demux.c index 45bcdd97..b6baf2f1 100644 --- a/src/lib/clapper/gst/clapper-uri-base-demux.c +++ b/src/lib/clapper/gst/clapper-uri-base-demux.c @@ -189,6 +189,20 @@ _make_handler_for_uri (ClapperUriBaseDemux *self, const gchar *uri, const gchar return element; } +static gboolean +_src_pad_query_func (GstPad *pad, GstObject *parent, GstQuery *query) +{ + if (GST_QUERY_TYPE (query) == GST_QUERY_CUSTOM) { + ClapperUriBaseDemux *self = CLAPPER_URI_BASE_DEMUX_CAST (parent); + ClapperUriBaseDemuxClass *uri_bd_class = CLAPPER_URI_BASE_DEMUX_GET_CLASS (self); + + if (uri_bd_class->handle_custom_query && uri_bd_class->handle_custom_query (self, query)) + return TRUE; + } + + return gst_pad_query_default (pad, parent, query); +} + gboolean clapper_uri_base_demux_set_uri (ClapperUriBaseDemux *self, const gchar *uri, const gchar *blacklisted_el) { @@ -254,6 +268,7 @@ clapper_uri_base_demux_set_uri (ClapperUriBaseDemux *self, const gchar *uri, con src_ghostpad = gst_ghost_pad_new_from_template ("src", priv->typefind_src, gst_element_class_get_pad_template (GST_ELEMENT_GET_CLASS (self), "src")); + gst_pad_set_query_function (src_ghostpad, (GstPadQueryFunction) _src_pad_query_func); gst_pad_set_active (src_ghostpad, TRUE); From 27fd2e0225f1e0d5b3af95d23adfaf44feb4887f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sat, 22 Nov 2025 20:59:13 +0100 Subject: [PATCH 09/12] clapper: Ensure redirects for an item are from single source In cases where there are multiple objects resolving a playlist at once (e.g. player starts playing playlist item that Media Scanner also works on), just ignore parsing results from the other parser. We keep/use parsing results from whichever parser parsed the playlist first. --- src/lib/clapper/clapper-media-item.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/lib/clapper/clapper-media-item.c b/src/lib/clapper/clapper-media-item.c index cffd2f55..ad43c99e 100644 --- a/src/lib/clapper/clapper-media-item.c +++ b/src/lib/clapper/clapper-media-item.c @@ -54,6 +54,7 @@ struct _ClapperMediaItem /* Whether using title from URI */ gboolean title_is_parsed; + GType redirects_src_type; gchar *redirect_uri; gchar *cache_uri; @@ -684,6 +685,8 @@ static inline gboolean clapper_media_item_set_redirect_uri (ClapperMediaItem *self, const gchar *redirect_uri, GstObject *redirect_src) { + GType src_type; + /* Safety checks */ if (G_UNLIKELY (!redirect_uri)) { GST_ERROR_OBJECT (self, "Received redirect request without an URI set"); @@ -694,6 +697,18 @@ clapper_media_item_set_redirect_uri (ClapperMediaItem *self, const gchar *redire return FALSE; } + /* Only allow redirects determined by the same source type, otherwise + * we would start mixing URIs when multiple sources message them */ + src_type = G_OBJECT_TYPE (redirect_src); + + if (self->redirects_src_type == 0) { + self->redirects_src_type = src_type; + } else if (self->redirects_src_type != src_type) { + GST_LOG_OBJECT (self, "Ignoring redirection from different source: %s", + G_OBJECT_TYPE_NAME (redirect_src)); + return FALSE; + } + g_set_str (&self->redirect_uri, redirect_uri); GST_DEBUG_OBJECT (self, "Set redirect URI: \"%s\", source: %s", self->redirect_uri, G_OBJECT_TYPE_NAME (redirect_src)); From 872c049af8f775a350b6947673496bc5f7de82e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sun, 23 Nov 2025 11:03:22 +0100 Subject: [PATCH 10/12] clapper: Skip default cache location set at item construction It is already NULL for newly created media item and otherwise debug printing for this property being changed to NULL for each media item is confusing --- src/lib/clapper/clapper-media-item.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/clapper/clapper-media-item.c b/src/lib/clapper/clapper-media-item.c index ad43c99e..1bd24c7d 100644 --- a/src/lib/clapper/clapper-media-item.c +++ b/src/lib/clapper/clapper-media-item.c @@ -770,6 +770,9 @@ clapper_media_item_update_from_parsed_playlist (ClapperMediaItem *self, GListSto void clapper_media_item_set_cache_location (ClapperMediaItem *self, const gchar *location) { + if (!self->cache_uri && !location) + return; // No change (called during construction) + g_clear_pointer (&self->cache_uri, g_free); if (location) From 9e1622976af90a729da903f0c07ff5bc187063a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sun, 23 Nov 2025 12:44:40 +0100 Subject: [PATCH 11/12] clapper: Make media item "cache-location" a readable property --- src/lib/clapper/clapper-enums.h | 2 + src/lib/clapper/clapper-media-item.c | 80 +++++++++++++++++++++++---- src/lib/clapper/clapper-media-item.h | 3 + src/lib/clapper/clapper-playbin-bus.c | 2 + src/lib/clapper/clapper-player.c | 3 + 5 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src/lib/clapper/clapper-enums.h b/src/lib/clapper/clapper-enums.h index 58ea281f..16236db6 100644 --- a/src/lib/clapper/clapper-enums.h +++ b/src/lib/clapper/clapper-enums.h @@ -173,6 +173,7 @@ typedef enum * @CLAPPER_REACTABLE_ITEM_UPDATED_DURATION: Media item duration was updated. * @CLAPPER_REACTABLE_ITEM_UPDATED_TIMELINE: Media item timeline was updated. * @CLAPPER_REACTABLE_ITEM_UPDATED_TAGS: Media item tags were updated. + * @CLAPPER_REACTABLE_ITEM_UPDATED_CACHE_LOCATION: Media item cache location was updated. * * Flags informing which properties were updated within [class@Clapper.MediaItem]. * @@ -184,6 +185,7 @@ typedef enum CLAPPER_REACTABLE_ITEM_UPDATED_DURATION = 1 << 1, CLAPPER_REACTABLE_ITEM_UPDATED_TIMELINE = 1 << 2, CLAPPER_REACTABLE_ITEM_UPDATED_TAGS = 1 << 3, + CLAPPER_REACTABLE_ITEM_UPDATED_CACHE_LOCATION = 1 << 4, } ClapperReactableItemUpdatedFlags; G_END_DECLS diff --git a/src/lib/clapper/clapper-media-item.c b/src/lib/clapper/clapper-media-item.c index 1bd24c7d..c4fc41ea 100644 --- a/src/lib/clapper/clapper-media-item.c +++ b/src/lib/clapper/clapper-media-item.c @@ -278,6 +278,31 @@ clapper_media_item_get_suburi (ClapperMediaItem *self) return suburi; } +/** + * clapper_media_item_get_cache_location: + * @item: a #ClapperMediaItem + * + * Get downloaded cache file location of #ClapperMediaItem. + * + * Returns: (transfer full) (type filename) (nullable): a cache file location of #ClapperMediaItem. + * + * Since: 0.10 + */ +gchar * +clapper_media_item_get_cache_location (ClapperMediaItem *self) +{ + gchar *cache_location = NULL; + + g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (self), NULL); + + GST_OBJECT_LOCK (self); + if (self->cache_uri) + cache_location = g_filename_from_uri (self->cache_uri, NULL, NULL); + GST_OBJECT_UNLOCK (self); + + return cache_location; +} + /** * clapper_media_item_get_title: * @item: a #ClapperMediaItem @@ -770,16 +795,42 @@ clapper_media_item_update_from_parsed_playlist (ClapperMediaItem *self, GListSto void clapper_media_item_set_cache_location (ClapperMediaItem *self, const gchar *location) { - if (!self->cache_uri && !location) - return; // No change (called during construction) + gboolean changed; - g_clear_pointer (&self->cache_uri, g_free); + GST_OBJECT_LOCK (self); - if (location) - self->cache_uri = g_filename_to_uri (location, NULL, NULL); + /* Skip if both are NULL (no change - called during construction) */ + if ((changed = (self->cache_uri || location))) { + g_clear_pointer (&self->cache_uri, g_free); - GST_DEBUG_OBJECT (self, "Set cache URI: \"%s\"", - GST_STR_NULL (self->cache_uri)); + if (location) + self->cache_uri = g_filename_to_uri (location, NULL, NULL); + + GST_DEBUG_OBJECT (self, "Set cache URI: \"%s\"", + GST_STR_NULL (self->cache_uri)); + } + + GST_OBJECT_UNLOCK (self); + + if (changed) { + ClapperPlayer *player; + + if ((player = clapper_player_get_from_ancestor (GST_OBJECT_CAST (self)))) { + ClapperFeaturesManager *features_manager; + + clapper_app_bus_post_prop_notify (player->app_bus, + GST_OBJECT_CAST (self), param_specs[PROP_CACHE_LOCATION]); + + if (player->reactables_manager) { + clapper_reactables_manager_trigger_item_updated (player->reactables_manager, self, + CLAPPER_REACTABLE_ITEM_UPDATED_CACHE_LOCATION); + } + if ((features_manager = clapper_player_get_features_manager (player))) + clapper_features_manager_trigger_item_updated (features_manager, self); + + gst_object_unref (player); + } + } } /* XXX: Can only be read from player thread. @@ -798,9 +849,6 @@ clapper_media_item_get_playback_uri (ClapperMediaItem *self) if (exists) return self->cache_uri; - - /* Do not test file existence next time */ - clapper_media_item_set_cache_location (self, NULL); } if (self->redirect_uri) @@ -914,6 +962,9 @@ clapper_media_item_get_property (GObject *object, guint prop_id, case PROP_SUBURI: g_value_take_string (value, clapper_media_item_get_suburi (self)); break; + case PROP_CACHE_LOCATION: + g_value_take_string (value, clapper_media_item_get_cache_location (self)); + break; case PROP_TAGS: g_value_take_boxed (value, clapper_media_item_get_tags (self)); break; @@ -980,11 +1031,18 @@ clapper_media_item_class_init (ClapperMediaItemClass *klass) * * Media downloaded cache file location. * + * This can be either set for newly created media items or + * it will be updated after download is completed if + * [property@Clapper.Player:download-enabled] is set. + * + * NOTE: This property was added in 0.8 as write at construct only. + * It can also be read only since Clapper 0.10. + * * Since: 0.8 */ param_specs[PROP_CACHE_LOCATION] = g_param_spec_string ("cache-location", NULL, NULL, NULL, - G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); /** * ClapperMediaItem:tags: diff --git a/src/lib/clapper/clapper-media-item.h b/src/lib/clapper/clapper-media-item.h index 8c8733a7..f2daf488 100644 --- a/src/lib/clapper/clapper-media-item.h +++ b/src/lib/clapper/clapper-media-item.h @@ -60,6 +60,9 @@ void clapper_media_item_set_suburi (ClapperMediaItem *item, const gchar *suburi) CLAPPER_API gchar * clapper_media_item_get_suburi (ClapperMediaItem *item); +CLAPPER_API +gchar * clapper_media_item_get_cache_location (ClapperMediaItem *item); + CLAPPER_API gchar * clapper_media_item_get_title (ClapperMediaItem *item); diff --git a/src/lib/clapper/clapper-playbin-bus.c b/src/lib/clapper/clapper-playbin-bus.c index 9e5476e5..1ee4aa08 100644 --- a/src/lib/clapper/clapper-playbin-bus.c +++ b/src/lib/clapper/clapper-playbin-bus.c @@ -957,6 +957,8 @@ _handle_element_msg (GstMessage *msg, ClapperPlayer *player) location = gst_structure_get_string (structure, "location"); signal_id = g_signal_lookup ("download-complete", CLAPPER_TYPE_PLAYER); + /* Set cache location before "download-complete" signal emit, + * so it can also be read directly from item */ GST_INFO_OBJECT (player, "Download of %" GST_PTR_FORMAT " complete: %s", downloaded_item, location); clapper_media_item_set_cache_location (downloaded_item, location); diff --git a/src/lib/clapper/clapper-player.c b/src/lib/clapper/clapper-player.c index 89602e7d..7fbf8a21 100644 --- a/src/lib/clapper/clapper-player.c +++ b/src/lib/clapper/clapper-player.c @@ -3012,6 +3012,9 @@ clapper_player_class_init (ClapperPlayerClass *klass) * be only emitted when progressive download buffering is enabled by * setting [property@Clapper.Player:download-enabled] property to %TRUE. * + * Download cache file location can also be read directly from @item + * through its [property@Clapper.MediaItem:cache-location] property. + * * Since: 0.8 */ signals[SIGNAL_DOWNLOAD_COMPLETE] = g_signal_new ("download-complete", From 50d7ae4b2aa0e40c2e582ba9b9b3b9ceff137bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= Date: Sun, 23 Nov 2025 14:23:17 +0100 Subject: [PATCH 12/12] clapper: Add readable "redirect-uri" property to media item Allow applications to inspect permanent redirect URIs of media items in a similar manner as done for "cache-location" property --- src/lib/clapper/clapper-enums.h | 4 +- src/lib/clapper/clapper-media-item.c | 120 ++++++++++++++++++--------- src/lib/clapper/clapper-media-item.h | 3 + 3 files changed, 89 insertions(+), 38 deletions(-) diff --git a/src/lib/clapper/clapper-enums.h b/src/lib/clapper/clapper-enums.h index 16236db6..12f12984 100644 --- a/src/lib/clapper/clapper-enums.h +++ b/src/lib/clapper/clapper-enums.h @@ -173,6 +173,7 @@ typedef enum * @CLAPPER_REACTABLE_ITEM_UPDATED_DURATION: Media item duration was updated. * @CLAPPER_REACTABLE_ITEM_UPDATED_TIMELINE: Media item timeline was updated. * @CLAPPER_REACTABLE_ITEM_UPDATED_TAGS: Media item tags were updated. + * @CLAPPER_REACTABLE_ITEM_UPDATED_REDIRECT_URI: Media item redirect URI was updated. * @CLAPPER_REACTABLE_ITEM_UPDATED_CACHE_LOCATION: Media item cache location was updated. * * Flags informing which properties were updated within [class@Clapper.MediaItem]. @@ -185,7 +186,8 @@ typedef enum CLAPPER_REACTABLE_ITEM_UPDATED_DURATION = 1 << 1, CLAPPER_REACTABLE_ITEM_UPDATED_TIMELINE = 1 << 2, CLAPPER_REACTABLE_ITEM_UPDATED_TAGS = 1 << 3, - CLAPPER_REACTABLE_ITEM_UPDATED_CACHE_LOCATION = 1 << 4, + CLAPPER_REACTABLE_ITEM_UPDATED_REDIRECT_URI = 1 << 4, + CLAPPER_REACTABLE_ITEM_UPDATED_CACHE_LOCATION = 1 << 5, } ClapperReactableItemUpdatedFlags; G_END_DECLS diff --git a/src/lib/clapper/clapper-media-item.c b/src/lib/clapper/clapper-media-item.c index c4fc41ea..cc36909c 100644 --- a/src/lib/clapper/clapper-media-item.c +++ b/src/lib/clapper/clapper-media-item.c @@ -75,6 +75,7 @@ enum PROP_ID, PROP_URI, PROP_SUBURI, + PROP_REDIRECT_URI, PROP_CACHE_LOCATION, PROP_TAGS, PROP_TITLE, @@ -203,12 +204,6 @@ clapper_media_item_get_id (ClapperMediaItem *self) return self->id; } -/* FIXME: 1.0: - * Consider change to be transfer-full and just return latest redirect URI - * (alternatively expose redirect URI). This should make it possible to work - * with enhancers that would benefit from knowledge about URI changes - * (e.g "Recall" could read actual media instead of playlist file). - */ /** * clapper_media_item_get_uri: * @item: a #ClapperMediaItem @@ -278,6 +273,30 @@ clapper_media_item_get_suburi (ClapperMediaItem *self) return suburi; } +/** + * clapper_media_item_get_redirect_uri: + * @item: a #ClapperMediaItem + * + * Get permanent redirect URI of #ClapperMediaItem. + * + * Returns: (transfer full) (nullable): a redirected URI of #ClapperMediaItem. + * + * Since: 0.10 + */ +gchar * +clapper_media_item_get_redirect_uri (ClapperMediaItem *self) +{ + gchar *redirect_uri; + + g_return_val_if_fail (CLAPPER_IS_MEDIA_ITEM (self), NULL); + + GST_OBJECT_LOCK (self); + redirect_uri = g_strdup (self->redirect_uri); + GST_OBJECT_UNLOCK (self); + + return redirect_uri; +} + /** * clapper_media_item_get_cache_location: * @item: a #ClapperMediaItem @@ -711,6 +730,7 @@ clapper_media_item_set_redirect_uri (ClapperMediaItem *self, const gchar *redire GstObject *redirect_src) { GType src_type; + gboolean changed; /* Safety checks */ if (G_UNLIKELY (!redirect_uri)) { @@ -734,11 +754,14 @@ clapper_media_item_set_redirect_uri (ClapperMediaItem *self, const gchar *redire return FALSE; } - g_set_str (&self->redirect_uri, redirect_uri); - GST_DEBUG_OBJECT (self, "Set redirect URI: \"%s\", source: %s", - self->redirect_uri, G_OBJECT_TYPE_NAME (redirect_src)); + GST_OBJECT_LOCK (self); + changed = g_set_str (&self->redirect_uri, redirect_uri); + GST_OBJECT_UNLOCK (self); - return TRUE; + GST_DEBUG_OBJECT (self, "Set redirect URI: \"%s\", source: %s", + redirect_uri, G_OBJECT_TYPE_NAME (redirect_src)); + + return changed; } gboolean @@ -747,38 +770,41 @@ clapper_media_item_update_from_parsed_playlist (ClapperMediaItem *self, GListSto { ClapperMediaItem *other_item; const gchar *redirect_uri; - gboolean title_changed = FALSE; + gboolean success, title_changed = FALSE; + /* First playlist item URI becomes a redirect URI for item to be updated */ other_item = g_list_model_get_item (G_LIST_MODEL (playlist), 0); redirect_uri = clapper_media_item_get_uri (other_item); - if (!clapper_media_item_set_redirect_uri (self, redirect_uri, playlist_src)) { - gst_object_unref (other_item); - return FALSE; - } - - GST_OBJECT_LOCK (other_item); - - if (other_item->tags) - clapper_media_item_update_from_tag_list (self, other_item->tags, player); - - /* Since its redirect now, we have to update title to describe new file instead of - * being a playlist title. If other item had parsed title, it also means that tags - * did not contain it, thus we have to manually update it and notify. */ - if (other_item->title_is_parsed) { - GST_OBJECT_LOCK (self); - title_changed = g_set_str (&self->title, other_item->title); - self->title_is_parsed = TRUE; - GST_OBJECT_UNLOCK (self); - } - - GST_OBJECT_UNLOCK (other_item); - - if (title_changed) { - ClapperReactableItemUpdatedFlags flags = CLAPPER_REACTABLE_ITEM_UPDATED_TITLE; + if ((success = clapper_media_item_set_redirect_uri (self, redirect_uri, playlist_src))) { ClapperFeaturesManager *features_manager; + ClapperReactableItemUpdatedFlags flags = CLAPPER_REACTABLE_ITEM_UPDATED_REDIRECT_URI; - clapper_app_bus_post_prop_notify (player->app_bus, GST_OBJECT_CAST (self), param_specs[PROP_TITLE]); + /* Notify about URI change before other properties */ + clapper_app_bus_post_prop_notify (player->app_bus, GST_OBJECT_CAST (self), + param_specs[PROP_REDIRECT_URI]); + + GST_OBJECT_LOCK (other_item); + + if (other_item->tags) + clapper_media_item_update_from_tag_list (self, other_item->tags, player); + + /* Since its redirect now, we have to update title to describe new file instead of + * being a playlist title. If other item had parsed title, it also means that tags + * did not contain it, thus we have to manually update it and notify. */ + if (other_item->title_is_parsed) { + GST_OBJECT_LOCK (self); + title_changed = g_set_str (&self->title, other_item->title); + self->title_is_parsed = TRUE; + GST_OBJECT_UNLOCK (self); + } + + GST_OBJECT_UNLOCK (other_item); + + if (title_changed) { + flags |= CLAPPER_REACTABLE_ITEM_UPDATED_TITLE; + clapper_app_bus_post_prop_notify (player->app_bus, GST_OBJECT_CAST (self), param_specs[PROP_TITLE]); + } if (player->reactables_manager) clapper_reactables_manager_trigger_item_updated (player->reactables_manager, self, flags); @@ -788,7 +814,7 @@ clapper_media_item_update_from_parsed_playlist (ClapperMediaItem *self, GListSto gst_object_unref (other_item); - return TRUE; + return success; } /* XXX: Must be set from player thread or upon construction */ @@ -962,6 +988,9 @@ clapper_media_item_get_property (GObject *object, guint prop_id, case PROP_SUBURI: g_value_take_string (value, clapper_media_item_get_suburi (self)); break; + case PROP_REDIRECT_URI: + g_value_take_string (value, clapper_media_item_get_redirect_uri (self)); + break; case PROP_CACHE_LOCATION: g_value_take_string (value, clapper_media_item_get_cache_location (self)); break; @@ -1026,6 +1055,23 @@ clapper_media_item_class_init (ClapperMediaItemClass *klass) NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * ClapperMediaItem:redirect-uri: + * + * Media permanent redirect URI. + * + * Changes when player determines a new redirect for given media item. + * This will also happen when item URI leads to a playlist. Once playlist + * is parsed, item is merged with the first item on that playlist and the + * remaining items are appended to the playback queue after that item position. + * + * Once redirect URI in item is present, player will use that URI instead + * of the default one. Cache location takes precedence over both URIs through. + */ + param_specs[PROP_REDIRECT_URI] = g_param_spec_string ("redirect-uri", + NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** * ClapperMediaItem:cache-location: * diff --git a/src/lib/clapper/clapper-media-item.h b/src/lib/clapper/clapper-media-item.h index f2daf488..1c77a807 100644 --- a/src/lib/clapper/clapper-media-item.h +++ b/src/lib/clapper/clapper-media-item.h @@ -60,6 +60,9 @@ void clapper_media_item_set_suburi (ClapperMediaItem *item, const gchar *suburi) CLAPPER_API gchar * clapper_media_item_get_suburi (ClapperMediaItem *item); +CLAPPER_API +gchar * clapper_media_item_get_redirect_uri (ClapperMediaItem *item); + CLAPPER_API gchar * clapper_media_item_get_cache_location (ClapperMediaItem *item);