entertainarr_adapter_filesystem_pcloud/
lib.rs1use 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}