use std::io::{Cursor, Read};
use serde::{Deserialize, Serialize};
use proton_sdk::error::{ProtonError, Result};
use proton_sdk::ids::NodeUid;
use proton_sdk::session::ProtonApiSession;
use crate::client::ProtonDriveClient;
use crate::node::{FileThumbnail, Node, Thumbnail, ThumbnailType};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PhotosTimelineItem {
pub uid: NodeUid,
pub capture_time: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(i32)]
pub enum PhotoTag {
Favorite = 0,
Screenshot = 1,
Video = 2,
LivePhoto = 3,
MotionPhoto = 4,
Selfie = 5,
Portrait = 6,
Burst = 7,
Panorama = 8,
Raw = 9,
}
#[derive(Debug, Clone, Default)]
pub struct PhotoUploadMetadata {
pub capture_time: Option<i64>,
pub main_photo_uid: Option<NodeUid>,
pub tags: Vec<PhotoTag>,
}
#[derive(Clone)]
pub struct ProtonPhotosClient {
drive: ProtonDriveClient,
}
impl ProtonPhotosClient {
pub fn new(session: &ProtonApiSession, mailbox_password: impl Into<Vec<u8>>) -> Self {
Self {
drive: ProtonDriveClient::new(session, mailbox_password),
}
}
pub fn from_drive_client(drive: ProtonDriveClient) -> Self {
Self { drive }
}
pub fn drive_client(&self) -> &ProtonDriveClient {
&self.drive
}
pub async fn get_photos_root(&self) -> Result<Option<Node>> {
self.drive.get_photos_root().await
}
pub async fn get_node(&self, uid: &NodeUid) -> Result<Option<Node>> {
self.drive.get_photos_node(uid).await
}
pub async fn enumerate_nodes(&self, uids: &[NodeUid]) -> Result<Vec<Node>> {
self.drive.enumerate_photos_nodes(uids).await
}
pub async fn enumerate_timeline(&self) -> Result<Vec<PhotosTimelineItem>> {
self.drive.enumerate_photos_timeline().await
}
pub async fn download_photo(&self, uid: &NodeUid) -> Result<Vec<u8>> {
let mut buf = Vec::new();
self.drive.download_photo_to(uid, &mut buf).await?;
Ok(buf)
}
pub async fn download_photo_to<W: std::io::Write>(
&self,
uid: &NodeUid,
output: &mut W,
) -> Result<()> {
self.drive.download_photo_to(uid, output).await
}
pub async fn upload_photo(
&self,
name: &str,
media_type: &str,
contents: &[u8],
metadata: PhotoUploadMetadata,
) -> Result<NodeUid> {
self.upload_photo_from(
name,
media_type,
Cursor::new(contents),
contents.len() as i64,
Vec::new(),
metadata,
false,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn upload_photo_from<R: Read + Send>(
&self,
name: &str,
media_type: &str,
reader: R,
intended_size: i64,
thumbnails: Vec<Thumbnail>,
metadata: PhotoUploadMetadata,
aead: bool,
) -> Result<NodeUid> {
self.drive
.upload_photo_from(
name,
media_type,
reader,
intended_size,
thumbnails,
&metadata,
aead,
)
.await
}
pub async fn download_thumbnail(
&self,
uid: &NodeUid,
thumbnail_type: ThumbnailType,
) -> Result<Option<Vec<u8>>> {
self.drive
.download_thumbnail_ctx(uid, thumbnail_type, true)
.await
}
pub async fn enumerate_thumbnails(
&self,
uids: &[NodeUid],
thumbnail_type: ThumbnailType,
) -> Result<Vec<FileThumbnail>> {
self.drive
.enumerate_thumbnails_ctx(uids, thumbnail_type, true)
.await
}
pub async fn find_duplicates(&self, _name: &str) -> Result<Vec<String>> {
Err(ProtonError::invalid_operation(
"find_duplicates is not implemented (parity with C# FindDuplicatesAsync)",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dtos::TimelinePhotoListResponse;
use proton_sdk::ids::VolumeId;
#[test]
fn timeline_item_round_trips() {
let item = PhotosTimelineItem {
uid: NodeUid::new(VolumeId::from("vol-1"), "link-9".into()),
capture_time: 1_700_000_000,
};
let json = serde_json::to_string(&item).unwrap();
let back: PhotosTimelineItem = serde_json::from_str(&json).unwrap();
assert_eq!(item, back);
}
#[test]
fn photo_tag_discriminants_match_csharp() {
assert_eq!(PhotoTag::Favorite as i32, 0);
assert_eq!(PhotoTag::Video as i32, 2);
assert_eq!(PhotoTag::Raw as i32, 9);
}
#[test]
fn timeline_response_deserializes_server_shape() {
let raw = r#"{
"Photos": [
{ "LinkID": "abc", "CaptureTime": 1700000000, "Hash": "deadbeef" },
{ "LinkID": "def", "CaptureTime": 1700000100, "Hash": "cafe", "ContentHash": "ff" }
]
}"#;
let parsed: TimelinePhotoListResponse = serde_json::from_str(raw).unwrap();
assert_eq!(parsed.photos.len(), 2);
assert_eq!(parsed.photos[0].id.to_string(), "abc");
assert_eq!(parsed.photos[0].capture_time, 1_700_000_000);
assert_eq!(parsed.photos[1].content_hash.as_deref(), Some("ff"));
}
}