Skip to main content

entertainarr_adapter_filesystem_pcloud/
lib.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use anyhow::Context;
5use entertainarr_adapter_filesystem_prelude::path::RootPath;
6use entertainarr_domain::media::entity::{MediaFileInput, VideoStreamResponse};
7use pcloud::entry::Entry;
8use pcloud::file::FileIdentifier;
9use pcloud::folder::FolderIdentifier;
10
11#[derive(Debug, serde::Deserialize)]
12pub struct Config {
13    #[serde(default = "Config::default_base_url")]
14    pub base_url: String,
15    pub credentials: pcloud::Credentials,
16    #[serde(default)]
17    pub base_path: PathBuf,
18}
19
20impl Config {
21    fn default_base_url() -> String {
22        pcloud::EU_REGION.to_string()
23    }
24
25    pub fn build(self) -> anyhow::Result<PcloudClient> {
26        Ok(PcloudClient(Arc::new(Client {
27            inner: pcloud::Client::builder()
28                .with_base_url(self.base_url)
29                .with_credentials(self.credentials)
30                .build()?,
31            base_path: RootPath::from(self.base_path),
32        })))
33    }
34}
35
36#[derive(Debug)]
37struct Client {
38    inner: pcloud::Client,
39    base_path: RootPath,
40}
41
42#[derive(Clone, Debug)]
43pub struct PcloudClient(Arc<Client>);
44
45impl PcloudClient {
46    fn absolute_path(&self, path: impl Into<PathBuf>) -> std::io::Result<PathBuf> {
47        self.0.base_path.absolute(path)
48    }
49
50    pub fn traverse(&self, path: &str) -> std::io::Result<PcloudWalkTree> {
51        Ok(PcloudWalkTree {
52            client: self.0.clone(),
53            entries: vec![WalkEntry::FolderPath {
54                parent: self.absolute_path(path)?,
55            }],
56        })
57    }
58
59    pub async fn video_stream(&self, path: PathBuf) -> anyhow::Result<Option<VideoStreamResponse>> {
60        let path = self.absolute_path(path)?;
61        let identifier = FileIdentifier::Path(path.to_string_lossy());
62        self.0
63            .inner
64            .get_file_link(identifier)
65            .await
66            .map(|res| {
67                res.first_link()
68                    .map(|link| VideoStreamResponse::Redirect(link.to_string()))
69            })
70            .context("unable to get video stream")
71    }
72}
73
74#[derive(Debug)]
75#[allow(clippy::large_enum_variant)]
76enum WalkEntry {
77    FolderPath {
78        parent: PathBuf,
79    },
80    Entry {
81        parent: PathBuf,
82        entry: pcloud::entry::Entry,
83    },
84}
85
86#[derive(Debug)]
87pub struct PcloudWalkTree {
88    client: Arc<Client>,
89    entries: Vec<WalkEntry>,
90}
91
92impl PcloudWalkTree {
93    pub async fn next(&mut self) -> anyhow::Result<Option<MediaFileInput>> {
94        while let Some(entry) = self.entries.pop() {
95            match entry {
96                WalkEntry::FolderPath { parent } => {
97                    let folder = self
98                        .client
99                        .inner
100                        .list_folder(FolderIdentifier::Path(parent.to_string_lossy()))
101                        .await?;
102                    self.entries
103                        .extend(folder.contents.into_iter().flatten().map(|entry| {
104                            WalkEntry::Entry {
105                                parent: parent.clone(),
106                                entry,
107                            }
108                        }));
109                }
110                WalkEntry::Entry { parent, entry } => match entry {
111                    Entry::Folder(folder) => {
112                        self.entries.push(WalkEntry::FolderPath {
113                            parent: parent.join(folder.base.name),
114                        });
115                    }
116                    Entry::File(file) => {
117                        return Ok(Some(MediaFileInput {
118                            filepath: self
119                                .client
120                                .base_path
121                                .relative(parent.join(&file.base.name))?,
122                            filename: file.base.name,
123                            content_type: file.content_type,
124                            file_size: file.size.unwrap_or_default() as u64,
125                            created_at: file.base.created,
126                            updated_at: file.base.modified,
127                        }));
128                    }
129                },
130            }
131        }
132        Ok(None)
133    }
134}