@@ -3,6 +3,7 @@ package config
33import (
44 "fmt"
55 "os"
6+ "path/filepath"
67 "strconv"
78 "strings"
89 "time"
@@ -13,9 +14,10 @@ type Config struct {
1314 Source PlexServerConfig `json:"source"`
1415 Destination PlexServerConfig `json:"destination"`
1516 SyncLabel string `json:"syncLabel"`
16- SourceReplaceFrom string `json:"sourceReplaceFrom"` // Optional: Source path pattern to replace (e.g., "/data/Movies")
17- SourceReplaceTo string `json:"sourceReplaceTo"` // Optional: Local path replacement (e.g., "M:\\Movies")
17+ SourceReplaceFrom string `json:"sourceReplaceFrom"` // Optional: Source path prefix to strip (e.g., "/data/Movies")
18+ SourceReplaceTo string `json:"sourceReplaceTo"` // Optional: Local path replacement (e.g., "/media/source"). Leave empty for same-volume mounting
1819 DestRootDir string `json:"destRootDir"` // Required: Destination root path (e.g., "/mnt/data/Movies")
20+ TransferMethod string `json:"transferMethod"` // Optional: Force transfer method ("rsync" or "scp"), auto-detected if empty
1921 Interval time.Duration `json:"interval"`
2022 SSH SSHConfig `json:"ssh"`
2123 Performance PerformanceConfig `json:"performance"`
@@ -82,11 +84,12 @@ func LoadConfig() (*Config, error) {
8284 SourceReplaceFrom : getEnvWithDefault ("SOURCE_REPLACE_FROM" , "" ),
8385 SourceReplaceTo : getEnvWithDefault ("SOURCE_REPLACE_TO" , "" ),
8486 DestRootDir : getEnvWithDefault ("DEST_ROOT_DIR" , "" ),
87+ TransferMethod : strings .ToLower (getEnvWithDefault ("TRANSFER_METHOD" , "" )), // rsync, scp, or empty for auto-detection
8588 SSH : SSHConfig {
86- User : getEnvWithDefault ("OPT_SSH_USER " , "" ),
87- Password : getEnvWithDefault ("OPT_SSH_PASSWORD " , "" ),
88- Port : getEnvWithDefault ("OPT_SSH_PORT " , "22" ),
89- KeyPath : getEnvWithDefault ("OPT_SSH_KEY_PATH " , "" ), // Keep for future use
89+ User : getEnvWithDefault ("SSH_USER " , "" ),
90+ Password : getEnvWithDefault ("SSH_PASSWORD " , "" ),
91+ Port : getEnvWithDefault ("SSH_PORT " , "22" ),
92+ KeyPath : getEnvWithDefault ("SSH_KEY_PATH " , "" ), // Keep for future use
9093 },
9194 DryRun : parseBoolEnv ("DRY_RUN" , false ),
9295 LogLevel : getEnvWithDefault ("LOG_LEVEL" , "INFO" ),
@@ -242,3 +245,78 @@ func parseFloatEnv(key string, defaultValue float64) float64 {
242245 }
243246 return defaultValue
244247}
248+
249+ // MapSourcePathToLocal converts a source Plex server path to a local filesystem path
250+ func (c * Config ) MapSourcePathToLocal (sourcePath string ) (string , error ) {
251+ if sourcePath == "" {
252+ return "" , fmt .Errorf ("source path is empty" )
253+ }
254+
255+ // If no source replacement configured, use the Plex path as-is
256+ if c .SourceReplaceFrom == "" {
257+ return filepath .FromSlash (sourcePath ), nil
258+ }
259+
260+ // If SourceReplaceFrom is set but SourceReplaceTo is empty,
261+ // use source path as-is (same volume mounting scenario)
262+ if c .SourceReplaceTo == "" {
263+ return filepath .FromSlash (sourcePath ), nil
264+ }
265+
266+ // Apply source replacement pattern
267+ sourcePathNorm := filepath .ToSlash (sourcePath )
268+ sourceReplaceFromNorm := filepath .ToSlash (c .SourceReplaceFrom )
269+
270+ if ! strings .HasPrefix (sourcePathNorm , sourceReplaceFromNorm ) {
271+ return "" , fmt .Errorf ("source path %s does not start with replacement pattern %s" , sourcePath , c .SourceReplaceFrom )
272+ }
273+
274+ relativePath := strings .TrimPrefix (sourcePathNorm , sourceReplaceFromNorm )
275+ relativePath = strings .TrimPrefix (relativePath , "/" )
276+
277+ localPath := filepath .Join (c .SourceReplaceTo , relativePath )
278+ return localPath , nil
279+ }
280+
281+ // MapLocalPathToDest converts a local filesystem path to a destination server path
282+ func (c * Config ) MapLocalPathToDest (localPath string ) (string , error ) {
283+ if localPath == "" {
284+ return "" , fmt .Errorf ("local path is empty" )
285+ }
286+
287+ if c .DestRootDir == "" {
288+ return "" , fmt .Errorf ("destination root directory not configured" )
289+ }
290+
291+ var relativePath string
292+
293+ if c .SourceReplaceTo != "" {
294+ // Standard case: strip SourceReplaceTo prefix from local path
295+ localPathNorm := filepath .ToSlash (localPath )
296+ sourceReplaceToNorm := filepath .ToSlash (c .SourceReplaceTo )
297+
298+ if ! strings .HasPrefix (localPathNorm , sourceReplaceToNorm ) {
299+ return "" , fmt .Errorf ("local path %s does not start with source replacement root %s" , localPath , c .SourceReplaceTo )
300+ }
301+
302+ relativePath = strings .TrimPrefix (localPathNorm , sourceReplaceToNorm )
303+ relativePath = strings .TrimPrefix (relativePath , "/" )
304+ } else if c .SourceReplaceFrom != "" {
305+ // Same volume mounting: strip SourceReplaceFrom prefix to get relative path
306+ localPathNorm := filepath .ToSlash (localPath )
307+ sourceReplaceFromNorm := filepath .ToSlash (c .SourceReplaceFrom )
308+
309+ if ! strings .HasPrefix (localPathNorm , sourceReplaceFromNorm ) {
310+ return "" , fmt .Errorf ("local path %s does not start with source replacement pattern %s" , localPath , c .SourceReplaceFrom )
311+ }
312+
313+ relativePath = strings .TrimPrefix (localPathNorm , sourceReplaceFromNorm )
314+ relativePath = strings .TrimPrefix (relativePath , "/" )
315+ } else {
316+ // Fallback: use just the filename (preserves original behavior)
317+ relativePath = filepath .Base (localPath )
318+ }
319+
320+ destPath := strings .TrimSuffix (c .DestRootDir , "/" ) + "/" + relativePath
321+ return destPath , nil
322+ }
0 commit comments