Skip to main content

caesura/utils/fs/
path_manager.rs

1use crate::prelude::*;
2
3/// Supported tracker suffixes for cross-tracker torrent duplication.
4const TRACKER_SUFFIXES: &[&str] = &["red", "ops", "pth"];
5
6#[injectable]
7pub struct PathManager {
8    shared_options: Ref<SharedOptions>,
9    cache_options: Ref<CacheOptions>,
10    file_options: Ref<FileOptions>,
11}
12
13impl PathManager {
14    /// Default user config file.
15    ///
16    /// - Docker: `/config.yml`
17    /// - Native: platform user config directory
18    #[must_use]
19    pub fn default_config_path() -> PathBuf {
20        if is_docker() {
21            return PathBuf::from("/config.yml");
22        }
23        dirs::config_dir()
24            .expect("config directory should be determinable")
25            .join(APP_NAME)
26            .join("config.yml")
27    }
28
29    /// Default user cache directory.
30    ///
31    /// - Docker: `/cache`
32    /// - Native: platform user cache directory
33    #[must_use]
34    pub fn default_cache_dir() -> PathBuf {
35        if is_docker() {
36            return PathBuf::from("/cache");
37        }
38        dirs::cache_dir()
39            .expect("cache directory should be determinable")
40            .join(APP_NAME)
41    }
42
43    /// Default output directory.
44    ///
45    /// - Docker: `/output`
46    /// - Native: platform user data directory
47    #[must_use]
48    pub fn default_output_dir() -> PathBuf {
49        if is_docker() {
50            return PathBuf::from("/output");
51        }
52        dirs::data_dir()
53            .expect("data directory should be determinable")
54            .join(APP_NAME)
55            .join("output")
56    }
57
58    #[must_use]
59    pub fn get_cache_dir(&self) -> PathBuf {
60        self.cache_options.path()
61    }
62
63    /// Path to the cached source `.torrent` file.
64    #[must_use]
65    pub fn get_source_torrent_path(&self, source: &Source) -> PathBuf {
66        let id = source.torrent.id;
67        let indexer = self.shared_options.indexer_lowercase();
68        self.get_cache_dir()
69            .join("torrents")
70            .join(format!("{id}.{indexer}.torrent"))
71    }
72
73    #[must_use]
74    pub fn get_output_dir(&self) -> PathBuf {
75        self.shared_options.output_path()
76    }
77
78    #[must_use]
79    pub fn get_spectrogram_dir(&self, source: &Source) -> PathBuf {
80        self.get_output_dir()
81            .join(SpectrogramName::get(&source.metadata))
82    }
83
84    #[must_use]
85    pub fn get_transcode_target_dir(&self, source: &Source, target: TargetFormat) -> PathBuf {
86        self.get_output_dir()
87            .join(TranscodeName::get(&source.metadata, target))
88    }
89
90    #[must_use]
91    pub fn get_transcode_path(
92        &self,
93        source: &Source,
94        target: TargetFormat,
95        flac: &FlacFile,
96    ) -> PathBuf {
97        let extension = target.get_file_extension();
98        let rename_tracks = self.file_options.rename_tracks;
99        // If rename_tracks enabled and disc context is set on flac, use renamed paths
100        let (base_name, sub_dir) = if rename_tracks && flac.disc_context.is_some() {
101            (
102                flac.renamed_file_stem(),
103                flac.renamed_sub_dir().unwrap_or_default(),
104            )
105        } else {
106            (flac.file_name.clone(), flac.sub_dir.clone())
107        };
108        self.get_transcode_target_dir(source, target)
109            .join(sub_dir)
110            .join(format!("{base_name}.{extension}"))
111    }
112
113    #[must_use]
114    pub fn get_torrent_path(&self, source: &Source, target: TargetFormat) -> PathBuf {
115        let indexer = self.shared_options.indexer_lowercase();
116        self.get_torrent_path_for_indexer(source, target, &indexer)
117    }
118
119    #[must_use]
120    fn get_torrent_path_for_indexer(
121        &self,
122        source: &Source,
123        target: TargetFormat,
124        indexer: &str,
125    ) -> PathBuf {
126        let mut filename = TranscodeName::get(&source.metadata, target);
127        filename.push('.');
128        filename.push_str(indexer);
129        filename.push_str(".torrent");
130        self.get_output_dir().join(filename)
131    }
132
133    /// Get the torrent path if it exists, or duplicate from another tracker's torrent.
134    ///
135    /// Example: `path/to/Artist - Album [2012] [WEB FLAC].red.torrent`
136    ///
137    /// Returns `None` if no torrent exists and none can be duplicated.
138    ///
139    /// Returns the torrent path if it already exists for the current indexer.
140    ///
141    /// Or attempts to duplicate from another tracker's torrent file
142    /// (e.g., `.ops.torrent`, `.pth.torrent`, `.red.torrent`).
143    ///
144    /// Returns the torrent path if duplication is successful.
145    pub async fn get_or_duplicate_existing_torrent_path(
146        &self,
147        source: &Source,
148        target: TargetFormat,
149    ) -> Result<Option<PathBuf>, Failure<TorrentCreateAction>> {
150        let target_path = self.get_torrent_path(source, target);
151        if target_path.is_file() {
152            return Ok(Some(target_path));
153        }
154        let fallback_path = self.find_fallback_torrent(source, target);
155        let Some(fallback_path) = fallback_path else {
156            return Ok(None);
157        };
158        let announce_url = self.shared_options.announce_url.clone();
159        let indexer = self.shared_options.indexer_lowercase();
160        TorrentCreator::duplicate(&fallback_path, &target_path, announce_url, indexer).await?;
161        Ok(Some(target_path))
162    }
163
164    /// Find a torrent file from another tracker that can be duplicated.
165    fn find_fallback_torrent(&self, source: &Source, target: TargetFormat) -> Option<PathBuf> {
166        let current_indexer = self.shared_options.indexer_lowercase();
167        for suffix in TRACKER_SUFFIXES {
168            if *suffix == current_indexer {
169                continue;
170            }
171            let path = self.get_torrent_path_for_indexer(source, target, suffix);
172            if path.is_file() {
173                return Some(path);
174            }
175        }
176        None
177    }
178}