photokit 0.4.5

Safe Rust bindings for Apple's Photos framework — photo library access on macOS
Documentation
use std::ptr;

use serde::{Deserialize, Serialize};

use crate::asset::{PHAsset, PHCoordinate};
use crate::collection_list::PHCollectionList;
use crate::error::PhotoKitError;
use crate::fetch_options::PHFetchOptions;
use crate::fetch_result::PHFetchResult;
use crate::ffi;
use crate::private::{cstring_from_str, json_cstring, parse_json_ptr};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Wraps `PHAssetCollectionType`.
pub enum PHAssetCollectionType {
    /// Case of `PHAssetCollectionType`.
    Album,
    /// Case of `PHAssetCollectionType`.
    SmartAlbum,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
/// Wraps `PHCollectionEditOperation`.
pub enum PHCollectionEditOperation {
    /// Case of `PHCollectionEditOperation`.
    DeleteContent,
    /// Case of `PHCollectionEditOperation`.
    RemoveContent,
    /// Case of `PHCollectionEditOperation`.
    AddContent,
    /// Case of `PHCollectionEditOperation`.
    CreateContent,
    /// Case of `PHCollectionEditOperation`.
    RearrangeContent,
    /// Case of `PHCollectionEditOperation`.
    Delete,
    /// Case of `PHCollectionEditOperation`.
    Rename,
}

impl PHCollectionEditOperation {
    pub(crate) const fn as_raw(self) -> i32 {
        match self {
            Self::DeleteContent => 1,
            Self::RemoveContent => 2,
            Self::AddContent => 3,
            Self::CreateContent => 4,
            Self::RearrangeContent => 5,
            Self::Delete => 6,
            Self::Rename => 7,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(transparent)]
/// Wraps `PHAssetCollectionSubtype`.
pub struct PHAssetCollectionSubtype(
    /// Raw value for `PHAssetCollectionSubtype`.
    pub i64,
);

impl PHAssetCollectionSubtype {
    /// Constant on `PHAssetCollectionSubtype`.
    pub const ALBUM_REGULAR: Self = Self(2);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const ALBUM_SYNCED_EVENT: Self = Self(3);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const ALBUM_SYNCED_FACES: Self = Self(4);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const ALBUM_SYNCED_ALBUM: Self = Self(5);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const ALBUM_IMPORTED: Self = Self(6);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const ALBUM_MY_PHOTO_STREAM: Self = Self(100);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const ALBUM_CLOUD_SHARED: Self = Self(101);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_GENERIC: Self = Self(200);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_PANORAMAS: Self = Self(201);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_VIDEOS: Self = Self(202);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_FAVORITES: Self = Self(203);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_TIMELAPSES: Self = Self(204);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_ALL_HIDDEN: Self = Self(205);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_RECENTLY_ADDED: Self = Self(206);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_BURSTS: Self = Self(207);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_SLOMO_VIDEOS: Self = Self(208);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_USER_LIBRARY: Self = Self(209);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_SELF_PORTRAITS: Self = Self(210);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_SCREENSHOTS: Self = Self(211);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_DEPTH_EFFECT: Self = Self(212);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_LIVE_PHOTOS: Self = Self(213);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_ANIMATED: Self = Self(214);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_LONG_EXPOSURES: Self = Self(215);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_UNABLE_TO_UPLOAD: Self = Self(216);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_RAW: Self = Self(217);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_CINEMATIC: Self = Self(218);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_SPATIAL: Self = Self(219);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const SMART_ALBUM_SCREEN_RECORDINGS: Self = Self(220);
    /// Constant on `PHAssetCollectionSubtype`.
    pub const ANY: Self = Self(i64::MAX);

    /// Returns the raw Photos framework value for `PHAssetCollectionSubtype`.
    pub const fn raw_value(self) -> i64 {
        self.0
    }
}

impl From<i64> for PHAssetCollectionSubtype {
    fn from(value: i64) -> Self {
        Self(value)
    }
}

impl From<PHAssetCollectionSubtype> for i64 {
    fn from(value: PHAssetCollectionSubtype) -> Self {
        value.0
    }
}

#[allow(clippy::unsafe_derive_deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Wraps `PHAssetCollection`.
pub struct PHAssetCollection {
    /// Corresponds to `PHAssetCollection.localIdentifier`.
    pub local_identifier: String,
    /// Corresponds to `PHAssetCollection.localizedTitle`.
    pub localized_title: Option<String>,
    /// Corresponds to `PHAssetCollection.collectionType`.
    pub collection_type: PHAssetCollectionType,
    /// Corresponds to `PHAssetCollection.collectionSubtype`.
    pub collection_subtype: PHAssetCollectionSubtype,
    /// Corresponds to `PHAssetCollection.estimatedAssetCount`.
    pub estimated_asset_count: Option<u64>,
    #[serde(default)]
    /// Corresponds to `PHAssetCollection.startDate`.
    pub start_date: Option<String>,
    #[serde(default)]
    /// Corresponds to `PHAssetCollection.endDate`.
    pub end_date: Option<String>,
    #[serde(default)]
    /// Corresponds to `PHAssetCollection.approximateLocation`.
    pub approximate_location: Option<PHCoordinate>,
    #[serde(default)]
    /// Corresponds to `PHAssetCollection.localizedLocationNames`.
    pub localized_location_names: Vec<String>,
    #[serde(default)]
    /// Corresponds to `PHAssetCollection.canContainAssets`.
    pub can_contain_assets: bool,
    #[serde(default)]
    /// Corresponds to `PHAssetCollection.canContainCollections`.
    pub can_contain_collections: bool,
}

impl PHAssetCollection {
    /// Wraps a Photos framework fetch operation on `PHAssetCollection`.
    pub fn fetch(fetch_options: &PHFetchOptions) -> Result<PHFetchResult<Self>, PhotoKitError> {
        let options_json = json_cstring(fetch_options, "PHFetchOptions")?;
        let mut error = ptr::null_mut();
        let payload =
            unsafe { ffi::ph_asset_collection_fetch_all_json(options_json.as_ptr(), &mut error) };
        if payload.is_null() {
            Err(unsafe { PhotoKitError::from_error_ptr(error, "fetch asset collections failed") })
        } else {
            let collections: Vec<Self> =
                unsafe { parse_json_ptr(payload, "PHAssetCollection list") }?;
            Ok(collections.into())
        }
    }

    /// Wraps a Photos framework fetch operation on `PHAssetCollection`.
    pub fn fetch_with_type(
        collection_type: PHAssetCollectionType,
        collection_subtype: impl Into<PHAssetCollectionSubtype>,
        fetch_options: &PHFetchOptions,
    ) -> Result<PHFetchResult<Self>, PhotoKitError> {
        let options_json = json_cstring(fetch_options, "PHFetchOptions")?;
        let collection_subtype = collection_subtype.into();
        let mut error = ptr::null_mut();
        let payload = unsafe {
            ffi::ph_asset_collection_fetch_with_type_json(
                match collection_type {
                    PHAssetCollectionType::Album => 1,
                    PHAssetCollectionType::SmartAlbum => 2,
                },
                collection_subtype.raw_value(),
                options_json.as_ptr(),
                &mut error,
            )
        };
        if payload.is_null() {
            Err(unsafe {
                PhotoKitError::from_error_ptr(error, "fetch asset collections by type failed")
            })
        } else {
            let collections: Vec<Self> =
                unsafe { parse_json_ptr(payload, "PHAssetCollection list") }?;
            Ok(collections.into())
        }
    }

    /// Wraps a Photos framework fetch operation on `PHAssetCollection`.
    pub fn fetch_with_local_identifiers(
        identifiers: &[String],
        fetch_options: &PHFetchOptions,
    ) -> Result<PHFetchResult<Self>, PhotoKitError> {
        let identifiers_json = json_cstring(identifiers, "collection identifiers")?;
        let options_json = json_cstring(fetch_options, "PHFetchOptions")?;
        let mut error = ptr::null_mut();
        let payload = unsafe {
            ffi::ph_asset_collection_fetch_with_local_identifiers_json(
                identifiers_json.as_ptr(),
                options_json.as_ptr(),
                &mut error,
            )
        };
        if payload.is_null() {
            Err(unsafe {
                PhotoKitError::from_error_ptr(
                    error,
                    "fetch asset collections by local identifier failed",
                )
            })
        } else {
            let collections: Vec<Self> =
                unsafe { parse_json_ptr(payload, "PHAssetCollection list") }?;
            Ok(collections.into())
        }
    }

    /// Wraps a Photos framework fetch operation on `PHAssetCollection`.
    pub fn fetch_containing_asset(
        asset: &PHAsset,
        collection_type: PHAssetCollectionType,
        fetch_options: &PHFetchOptions,
    ) -> Result<PHFetchResult<Self>, PhotoKitError> {
        let asset_identifier = cstring_from_str(&asset.local_identifier, "asset local identifier")?;
        let options_json = json_cstring(fetch_options, "PHFetchOptions")?;
        let mut error = ptr::null_mut();
        let payload = unsafe {
            ffi::ph_asset_collection_fetch_containing_asset_json(
                asset_identifier.as_ptr(),
                match collection_type {
                    PHAssetCollectionType::Album => 1,
                    PHAssetCollectionType::SmartAlbum => 2,
                },
                options_json.as_ptr(),
                &mut error,
            )
        };
        if payload.is_null() {
            Err(unsafe {
                PhotoKitError::from_error_ptr(
                    error,
                    "fetch asset collections containing asset failed",
                )
            })
        } else {
            let collections: Vec<Self> =
                unsafe { parse_json_ptr(payload, "PHAssetCollection list") }?;
            Ok(collections.into())
        }
    }

    /// Looks up `PHAssetCollection` from Photos framework identifiers.
    pub fn from_local_identifier(
        local_identifier: impl Into<String>,
    ) -> Result<Option<Self>, PhotoKitError> {
        let result = Self::fetch_with_local_identifiers(
            &[local_identifier.into()],
            &PHFetchOptions::default(),
        )?;
        Ok(result.into_vec().into_iter().next())
    }

    /// Queries Photos framework state exposed by `PHAssetCollection`.
    pub fn can_perform_edit_operation(
        &self,
        edit_operation: PHCollectionEditOperation,
    ) -> Result<bool, PhotoKitError> {
        let collection_identifier =
            cstring_from_str(&self.local_identifier, "collection local identifier")?;
        let mut error = ptr::null_mut();
        let allowed = unsafe {
            ffi::ph_asset_collection_can_perform_edit_operation(
                collection_identifier.as_ptr(),
                edit_operation.as_raw(),
                &mut error,
            )
        };
        if error.is_null() {
            Ok(allowed != 0)
        } else {
            Err(unsafe {
                PhotoKitError::from_error_ptr(
                    error,
                    "asset collection edit capability lookup failed",
                )
            })
        }
    }

    /// Wraps a Photos framework operation on `PHAssetCollection`.
    pub fn assets(
        &self,
        fetch_options: &PHFetchOptions,
    ) -> Result<PHFetchResult<PHAsset>, PhotoKitError> {
        PHAsset::fetch_in_asset_collection(self, fetch_options)
    }

    /// Wraps a Photos framework operation on `PHAssetCollection`.
    pub fn containing_collection_lists(
        &self,
        fetch_options: &PHFetchOptions,
    ) -> Result<PHFetchResult<PHCollectionList>, PhotoKitError> {
        PHCollectionList::fetch_containing_collection_local_identifier(
            &self.local_identifier,
            fetch_options,
        )
    }
}