Skip to main content

media_remote/utils/
helpers.rs

1#[cfg(feature = "artwork")]
2use std::io::Cursor;
3
4use block2::RcBlock;
5use objc2::rc::{autoreleasepool, Retained};
6
7#[cfg(feature = "artwork")]
8use image::ImageReader;
9#[cfg(feature = "artwork")]
10use {
11    objc2::{runtime::AnyObject, AnyThread},
12    objc2_app_kit::{
13        NSBitmapImageFileType, NSBitmapImageRep, NSBitmapImageRepPropertyKey, NSWorkspace,
14    },
15    objc2_foundation::{
16        NSDictionary, NSFileManager, NSNotification, NSNotificationCenter, NSString,
17    },
18};
19
20#[cfg(not(feature = "artwork"))]
21use {
22    objc2_app_kit::NSWorkspace,
23    objc2_foundation::{NSFileManager, NSNotification, NSNotificationCenter, NSString},
24};
25
26use std::ptr::NonNull;
27
28#[allow(unused_imports)]
29use crate::{register_for_now_playing_notifications, BundleInfo, Notification, Observer};
30
31// Retrieves information about an application based on its bundle identifier.
32///
33///
34/// If the application is found, its name and icon are returned as a `BundleInfo` struct.
35/// Otherwise, `None` is returned if the application cannot be located or an error occurs
36/// during data retrieval.
37///
38/// # Arguments
39/// - `id`: A string slice representing the application's bundle identifier.
40///
41/// # Returns
42/// - `Option<BundleInfo>`:
43///     - `Some(BundleInfo)` containing the application's name and icon if retrieval is successful.
44///     - `None` if the application cannot be found or if an error occurs during processing.
45///
46/// # Example
47/// ```rust
48/// use media_remote::{get_now_playing_client_parent_app_bundle_identifier, get_bundle_info};
49///
50/// let bundle_id = get_now_playing_client_parent_app_bundle_identifier();
51///
52/// if let Some(id) = bundle_id {
53///     if let Some(bundle) = get_bundle_info(id.as_str()) {
54///         println!("App Name: {}", bundle.name);
55///     } else {
56///         println!("Application not found.");
57///     }
58/// }
59/// ```
60pub fn get_bundle_info(id: &str) -> Option<BundleInfo> {
61    autoreleasepool(|_| {
62        let workspace = NSWorkspace::sharedWorkspace();
63        let url = workspace.URLForApplicationWithBundleIdentifier(&NSString::from_str(id))?;
64
65        let path = &url.path()?;
66
67        let file_manager = NSFileManager::defaultManager();
68        let name = file_manager.displayNameAtPath(path);
69
70        #[cfg(feature = "artwork")]
71        let icon = {
72            let image = workspace.iconForFile(path);
73
74            unsafe {
75                let cg_image =
76                    image.CGImageForProposedRect_context_hints(std::ptr::null_mut(), None, None)?;
77                let bitmap_rep = NSBitmapImageRep::alloc();
78                let bitmap_rep = NSBitmapImageRep::initWithCGImage(bitmap_rep, &cg_image);
79                let props = NSDictionary::<NSBitmapImageRepPropertyKey, AnyObject>::new();
80                let data = bitmap_rep
81                    .representationUsingType_properties(NSBitmapImageFileType::PNG, &props)?;
82
83                Some(
84                    ImageReader::new(Cursor::new(data.to_vec()))
85                        .with_guessed_format()
86                        .ok()?
87                        .decode()
88                        .ok()?,
89                )
90            }
91        };
92
93        Some(BundleInfo {
94            name: name.to_string(),
95            #[cfg(feature = "artwork")]
96            icon,
97        })
98    })
99}
100
101/// Adds an observer for a specific media notification.
102///
103/// This function registers a closure to be executed when the specified `notification`
104/// is posted. It listens for notifications related to media playback state changes.
105///
106/// **Note:** [`register_for_now_playing_notifications`] **must** be called before using
107/// this function to ensure notifications are received.
108///
109/// # Arguments
110/// - `notification`: The [`Notification`] type representing the event to observe.
111/// - `closure`: A closure to execute when the notification is received.
112///
113/// # Returns
114/// - An [`Observer`] handle that can be used to remove the observer later.
115///
116/// # Example
117/// ```rust
118/// use media_remote::{register_for_now_playing_notifications, add_observer, Notification};
119///
120/// register_for_now_playing_notifications();
121///
122/// let observer = add_observer(Notification::NowPlayingApplicationIsPlayingDidChange, || {
123///     println!("Now playing status changed.");
124/// });
125/// ```
126pub fn add_observer<F: Fn() + 'static>(notification: Notification, closure: F) -> Observer {
127    unsafe {
128        let observer = NSNotificationCenter::defaultCenter()
129            .addObserverForName_object_queue_usingBlock(
130                Some(NSString::from_str(notification.as_str()).as_ref()),
131                None,
132                None,
133                &RcBlock::new(move |_: NonNull<NSNotification>| closure()),
134            );
135
136        Retained::cast_unchecked(observer)
137    }
138}
139
140/// Removes a previously added observer.
141///
142/// This function removes an observer registered with [`add_observer`], preventing further
143/// notifications from being received.
144///
145/// # Arguments
146/// - `observer`: The [`Observer`] handle returned from [`add_observer`].
147///
148/// # Example
149/// ```rust
150/// use media_remote::{register_for_now_playing_notifications, add_observer, remove_observer, Notification};
151///
152/// register_for_now_playing_notifications();
153///
154/// let observer = add_observer(Notification::NowPlayingApplicationIsPlayingDidChange, || {
155///     println!("Now playing status changed.");
156/// });
157///
158/// // Later, when no longer needed:
159/// remove_observer(observer);
160/// ```
161pub fn remove_observer(observer: Observer) {
162    unsafe {
163        NSNotificationCenter::defaultCenter().removeObserver(&observer);
164    }
165}