Skip to content

Commit 3076fd4

Browse files
committed
Refactor media source path configuration
Introduced support for named media source paths with optional metadata like original path and extension. Added backward compatibility for legacy configuration and updated Helm chart values and documentation to reflect the new structure.
1 parent 50aafea commit 3076fd4

File tree

4 files changed

+243
-11
lines changed

4 files changed

+243
-11
lines changed

charts/shoebox/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ The following table lists the configurable parameters of the Shoebox chart and t
6464
| `config.serverHost` | Host to bind the server | `0.0.0.0` |
6565
| `config.serverPort` | Port to bind the server | `3000` |
6666
| `config.databaseUrl` | Database URL (SQLite) | `sqlite:/app/data/videos.db` |
67-
| `config.mediaSourcePaths` | Paths to scan for videos | `/mnt/videos` |
67+
| `config.mediaSourcePaths` | Paths to scan for videos (supports named sections, see docs) | `/mnt/videos` |
6868
| `config.thumbnailPath` | Path to store thumbnails | `/app/thumbnails` |
6969
| `config.exportBasePath` | Path for exported files | `/app/exports` |
7070
| `config.rustLog` | Rust log level | `info` |

charts/shoebox/values.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,17 @@ config:
9292
serverHost: "0.0.0.0"
9393
serverPort: 3000
9494
databaseUrl: "sqlite:/app/data/videos.db"
95-
mediaSourcePaths: "/mnt/videos"
95+
# Media source paths configuration
96+
# You can configure multiple media sources with different settings
97+
mediaSourcePaths:
98+
sources:
99+
- name: bmpcc
100+
path: /mnt/videos
101+
originalPath: /home/user/videos
102+
originalExtension: mp4
103+
- name: gopro
104+
path: /mnt/other-videos
105+
originalPath: /media/external/videos
96106
thumbnailPath: "/app/thumbnails"
97107
exportBasePath: "/app/exports"
98108
rustLog: "info"

src/config.rs

Lines changed: 215 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,67 @@ pub struct DatabaseConfig {
2323

2424
#[derive(Clone, Debug, Deserialize, Serialize)]
2525
pub struct MediaPathConfig {
26+
pub name: Option<String>,
2627
pub path: String,
2728
pub original_path: Option<String>,
2829
pub original_extension: Option<String>,
2930
}
3031

32+
#[derive(Clone, Debug, Deserialize, Serialize)]
33+
pub struct MediaSourceConfig {
34+
pub name: String,
35+
pub path: String,
36+
#[serde(rename = "originalPath")]
37+
pub original_path: Option<String>,
38+
#[serde(rename = "originalExtension")]
39+
pub original_extension: Option<String>,
40+
}
41+
42+
#[derive(Clone, Debug, Deserialize, Serialize)]
43+
pub struct MediaSourcePathsConfig {
44+
pub sources: Option<Vec<MediaSourceConfig>>,
45+
#[serde(skip)]
46+
pub legacy_string: Option<String>,
47+
}
48+
3149
#[derive(Clone, Debug, Deserialize, Serialize)]
3250
pub struct MediaConfig {
51+
#[serde(rename = "mediaSourcePaths")]
52+
pub media_source_paths_config: MediaSourcePathsConfig,
3353
pub source_paths: Vec<MediaPathConfig>,
54+
#[serde(rename = "exportBasePath")]
3455
pub export_base_path: String,
56+
#[serde(rename = "thumbnailPath")]
3557
pub thumbnail_path: String,
3658
}
3759

60+
impl MediaConfig {
61+
// Convert MediaSourcePathsConfig to Vec<MediaPathConfig>
62+
pub fn convert_source_paths(&mut self) {
63+
if let Some(sources) = &self.media_source_paths_config.sources {
64+
// Convert each MediaSourceConfig to MediaPathConfig
65+
self.source_paths = sources.iter().map(|source| {
66+
MediaPathConfig {
67+
name: Some(source.name.clone()),
68+
path: source.path.clone(),
69+
original_path: source.original_path.clone(),
70+
original_extension: source.original_extension.clone(),
71+
}
72+
}).collect();
73+
} else if let Some(legacy_string) = &self.media_source_paths_config.legacy_string {
74+
// If we have a legacy string, parse it
75+
self.source_paths = parse_comma_separated_paths_from_string(legacy_string);
76+
} else {
77+
// If no sources are defined, use environment variables as fallback
78+
self.source_paths = parse_comma_separated_paths("MEDIA_SOURCE_PATHS");
79+
}
80+
}
81+
}
82+
3883
impl Config {
3984
pub fn load() -> Result<Self> {
4085
// Default configuration
41-
let config = Config {
86+
let mut config = Config {
4287
server: ServerConfig {
4388
host: env::var("SERVER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()),
4489
port: env::var("SERVER_PORT")
@@ -54,26 +99,179 @@ impl Config {
5499
.unwrap_or(5),
55100
},
56101
media: MediaConfig {
57-
source_paths: parse_comma_separated_paths("MEDIA_SOURCE_PATHS"),
102+
media_source_paths_config: MediaSourcePathsConfig {
103+
sources: None,
104+
legacy_string: env::var("MEDIA_SOURCE_PATHS").ok(),
105+
},
106+
source_paths: Vec::new(), // Will be populated in convert_source_paths
58107
export_base_path: env::var("EXPORT_BASE_PATH")
59108
.unwrap_or_else(|_| "./exports".to_string()),
60109
thumbnail_path: env::var("THUMBNAIL_PATH")
61110
.unwrap_or_else(|_| "./thumbnails".to_string()),
62111
},
63112
};
64113

114+
// Convert media_source_paths_config to source_paths
115+
config.media.convert_source_paths();
116+
65117
Ok(config)
66118
}
67119
}
68120

121+
fn parse_comma_separated_paths_from_string(paths_str: &str) -> Vec<MediaPathConfig> {
122+
paths_str
123+
.split(',')
124+
.map(|s| s.trim().to_string())
125+
.map(|path_config| {
126+
// Check if the path contains a named section (e.g., "bmpcc:")
127+
if let Some(colon_pos) = path_config.find(':') {
128+
let name = path_config[..colon_pos].trim().to_string();
129+
let config_str = path_config[colon_pos + 1..].trim().to_string();
130+
131+
// Split the configuration by semicolons
132+
let parts: Vec<&str> = config_str.split(';').collect();
133+
134+
if parts.is_empty() {
135+
// Invalid configuration, return a default
136+
return MediaPathConfig {
137+
name: Some(name),
138+
path: "./media".to_string(),
139+
original_path: None,
140+
original_extension: None,
141+
};
142+
}
143+
144+
let path = parts[0].to_string();
145+
146+
// Parse original_path if provided
147+
let original_path = parts.get(1)
148+
.filter(|&p| !p.is_empty())
149+
.map(|p| p.to_string());
150+
151+
// Parse original_extension if provided
152+
let original_extension = parts.get(2)
153+
.filter(|&e| !e.is_empty())
154+
.map(|e| e.to_string());
155+
156+
// If original_path is provided without extension, use the same extension as path
157+
let original_extension = if original_path.is_some() && original_extension.is_none() {
158+
// Extract extension from path
159+
std::path::Path::new(&path)
160+
.extension()
161+
.and_then(|ext| ext.to_str())
162+
.map(|ext| ext.to_string())
163+
} else {
164+
original_extension
165+
};
166+
167+
MediaPathConfig {
168+
name: Some(name),
169+
path,
170+
original_path,
171+
original_extension,
172+
}
173+
}
174+
// Backward compatibility: Check if the path contains configuration options without a name
175+
else if path_config.contains(';') {
176+
let parts: Vec<&str> = path_config.split(';').collect();
177+
let path = parts[0].to_string();
178+
179+
// Parse original_path if provided
180+
let original_path = parts.get(1)
181+
.filter(|&p| !p.is_empty())
182+
.map(|p| p.to_string());
183+
184+
// Parse original_extension if provided
185+
let original_extension = parts.get(2)
186+
.filter(|&e| !e.is_empty())
187+
.map(|e| e.to_string());
188+
189+
// If original_path is provided without extension, use the same extension as path
190+
let original_extension = if original_path.is_some() && original_extension.is_none() {
191+
// Extract extension from path
192+
std::path::Path::new(&path)
193+
.extension()
194+
.and_then(|ext| ext.to_str())
195+
.map(|ext| ext.to_string())
196+
} else {
197+
original_extension
198+
};
199+
200+
MediaPathConfig {
201+
name: None,
202+
path,
203+
original_path,
204+
original_extension,
205+
}
206+
} else {
207+
// Simple path without additional configuration
208+
MediaPathConfig {
209+
name: None,
210+
path: path_config,
211+
original_path: None,
212+
original_extension: None,
213+
}
214+
}
215+
})
216+
.collect()
217+
}
218+
69219
fn parse_comma_separated_paths(env_var: &str) -> Vec<MediaPathConfig> {
70220
env::var(env_var)
71221
.unwrap_or_else(|_| "./media".to_string())
72222
.split(',')
73223
.map(|s| s.trim().to_string())
74224
.map(|path_config| {
75-
// Check if the path contains configuration options
76-
if path_config.contains(';') {
225+
// Check if the path contains a named section (e.g., "bmpcc:")
226+
if let Some(colon_pos) = path_config.find(':') {
227+
let name = path_config[..colon_pos].trim().to_string();
228+
let config_str = path_config[colon_pos + 1..].trim().to_string();
229+
230+
// Split the configuration by semicolons
231+
let parts: Vec<&str> = config_str.split(';').collect();
232+
233+
if parts.is_empty() {
234+
// Invalid configuration, return a default
235+
return MediaPathConfig {
236+
name: Some(name),
237+
path: "./media".to_string(),
238+
original_path: None,
239+
original_extension: None,
240+
};
241+
}
242+
243+
let path = parts[0].to_string();
244+
245+
// Parse original_path if provided
246+
let original_path = parts.get(1)
247+
.filter(|&p| !p.is_empty())
248+
.map(|p| p.to_string());
249+
250+
// Parse original_extension if provided
251+
let original_extension = parts.get(2)
252+
.filter(|&e| !e.is_empty())
253+
.map(|e| e.to_string());
254+
255+
// If original_path is provided without extension, use the same extension as path
256+
let original_extension = if original_path.is_some() && original_extension.is_none() {
257+
// Extract extension from path
258+
std::path::Path::new(&path)
259+
.extension()
260+
.and_then(|ext| ext.to_str())
261+
.map(|ext| ext.to_string())
262+
} else {
263+
original_extension
264+
};
265+
266+
MediaPathConfig {
267+
name: Some(name),
268+
path,
269+
original_path,
270+
original_extension,
271+
}
272+
}
273+
// Backward compatibility: Check if the path contains configuration options without a name
274+
else if path_config.contains(';') {
77275
let parts: Vec<&str> = path_config.split(';').collect();
78276
let path = parts[0].to_string();
79277

@@ -87,14 +285,27 @@ fn parse_comma_separated_paths(env_var: &str) -> Vec<MediaPathConfig> {
87285
.filter(|&e| !e.is_empty())
88286
.map(|e| e.to_string());
89287

288+
// If original_path is provided without extension, use the same extension as path
289+
let original_extension = if original_path.is_some() && original_extension.is_none() {
290+
// Extract extension from path
291+
std::path::Path::new(&path)
292+
.extension()
293+
.and_then(|ext| ext.to_str())
294+
.map(|ext| ext.to_string())
295+
} else {
296+
original_extension
297+
};
298+
90299
MediaPathConfig {
300+
name: None,
91301
path,
92302
original_path,
93303
original_extension,
94304
}
95305
} else {
96306
// Simple path without additional configuration
97307
MediaPathConfig {
308+
name: None,
98309
path: path_config,
99310
original_path: None,
100311
original_extension: None,

src/services/scanner.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,18 +202,29 @@ impl ScannerService {
202202
None
203203
};
204204

205-
// Check for original file if original_path and original_extension are specified
206-
let original_file_path = if let (Some(original_path), Some(original_extension)) =
207-
(&path_config.original_path, &path_config.original_extension) {
208-
205+
// Check for original file if original_path is specified
206+
let original_file_path = if let Some(original_path) = &path_config.original_path {
209207
// Get the file name without extension
210208
let file_stem = std::path::Path::new(&file_name)
211209
.file_stem()
212210
.map(|s| s.to_string_lossy().to_string());
213211

214212
if let Some(stem) = file_stem {
213+
// Determine the extension to use
214+
let extension = if let Some(original_extension) = &path_config.original_extension {
215+
// Use the specified original extension
216+
original_extension.clone()
217+
} else {
218+
// Use the extension from the scan path
219+
std::path::Path::new(&file_path)
220+
.extension()
221+
.and_then(|ext| ext.to_str())
222+
.unwrap_or("mp4")
223+
.to_string()
224+
};
225+
215226
// Construct the path to the original file
216-
let original_file = format!("{}/{}.{}", original_path, stem, original_extension);
227+
let original_file = format!("{}/{}.{}", original_path, stem, extension);
217228
let original_path_buf = std::path::Path::new(&original_file);
218229

219230
// Check if the original file exists

0 commit comments

Comments
 (0)