Suppression de la synchro bloquante morte et des dépendances inutilisées
- suppression de src/rss/sync.rs : l'implémentation de synchronisation bloquante n'était plus appelée que par du code mort de app.rs ; la chaîne vivante reste sync_task -> async_sync -> downloader/parser - suppression des méthodes mortes save_feeds / sync_all_feeds / sync_single_feed de RssPodcastApp (app.rs) et de leurs import - suppression consécutive de set_error / set_info / set_sync_state dans AppState (plus aucun appelant) - Cargo.toml : retrait de tokio, md5, pathdiff, diligent-date-parser, mp3-metadata et futures-util (jamais référencées dans le code) ; tempfile passe en dev-dependencies ; features reqwest réduites à blocking (json/stream inutilisées)
90dae95 parent: 812d86c modified
Cargo.lock +0 -54 | @@ -1395,7 +1395,6 @@ dependencies = [ | ||
| 1395 | 1395 | "futures-core", |
| 1396 | 1396 | "futures-io", |
| 1397 | 1397 | "futures-macro", |
| 1398 | - "futures-sink", | |
| 1399 | 1398 | "futures-task", |
| 1400 | 1399 | "memchr", |
| 1401 | 1400 | "pin-project-lite", |
| @@ -2234,12 +2233,6 @@ dependencies = [ | ||
| 2234 | 2233 | "libc", |
| 2235 | 2234 | ] |
| 2236 | 2235 | |
| 2237 | -[[package]] | |
| 2238 | -name = "md5" | |
| 2239 | -version = "0.7.0" | |
| 2240 | -source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2241 | -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" | |
| 2242 | - | |
| 2243 | 2236 | [[package]] |
| 2244 | 2237 | name = "memchr" |
| 2245 | 2238 | version = "2.8.2" |
| @@ -2301,12 +2294,6 @@ dependencies = [ | ||
| 2301 | 2294 | "pxfm", |
| 2302 | 2295 | ] |
| 2303 | 2296 | |
| 2304 | -[[package]] | |
| 2305 | -name = "mp3-metadata" | |
| 2306 | -version = "0.3.4" | |
| 2307 | -source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2308 | -checksum = "e4eaa3a228dc218005d769b88ce3a0310f49152f2029125289e94c49474c9cc4" | |
| 2309 | - | |
| 2310 | 2297 | [[package]] |
| 2311 | 2298 | name = "naga" |
| 2312 | 2299 | version = "29.0.4" |
| @@ -2884,12 +2871,6 @@ dependencies = [ | ||
| 2884 | 2871 | "windows-link", |
| 2885 | 2872 | ] |
| 2886 | 2873 | |
| 2887 | -[[package]] | |
| 2888 | -name = "pathdiff" | |
| 2889 | -version = "0.2.3" | |
| 2890 | -source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2891 | -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" | |
| 2892 | - | |
| 2893 | 2874 | [[package]] |
| 2894 | 2875 | name = "peniko" |
| 2895 | 2876 | version = "0.6.1" |
| @@ -3283,12 +3264,10 @@ dependencies = [ | ||
| 3283 | 3264 | "system-configuration", |
| 3284 | 3265 | "tokio", |
| 3285 | 3266 | "tokio-native-tls", |
| 3286 | - "tokio-util", | |
| 3287 | 3267 | "tower-service", |
| 3288 | 3268 | "url", |
| 3289 | 3269 | "wasm-bindgen", |
| 3290 | 3270 | "wasm-bindgen-futures", |
| 3291 | - "wasm-streams", | |
| 3292 | 3271 | "web-sys", |
| 3293 | 3272 | "winreg", |
| 3294 | 3273 | ] |
| @@ -3332,16 +3311,11 @@ name = "rss-podcast-dl-egui" | ||
| 3332 | 3311 | version = "0.2.0" |
| 3333 | 3312 | dependencies = [ |
| 3334 | 3313 | "chrono", |
| 3335 | - "diligent-date-parser", | |
| 3336 | 3314 | "eframe", |
| 3337 | 3315 | "egui", |
| 3338 | 3316 | "env_logger", |
| 3339 | - "futures-util", | |
| 3340 | 3317 | "id3", |
| 3341 | 3318 | "log", |
| 3342 | - "md5", | |
| 3343 | - "mp3-metadata", | |
| 3344 | - "pathdiff", | |
| 3345 | 3319 | "reqwest", |
| 3346 | 3320 | "rfd", |
| 3347 | 3321 | "rss", |
| @@ -3349,7 +3323,6 @@ dependencies = [ | ||
| 3349 | 3323 | "serde_json", |
| 3350 | 3324 | "tempfile", |
| 3351 | 3325 | "thiserror 1.0.69", |
| 3352 | - "tokio", | |
| 3353 | 3326 | "uuid", |
| 3354 | 3327 | ] |
| 3355 | 3328 | |
| @@ -3962,25 +3935,11 @@ dependencies = [ | ||
| 3962 | 3935 | "bytes", |
| 3963 | 3936 | "libc", |
| 3964 | 3937 | "mio", |
| 3965 | - "parking_lot", | |
| 3966 | 3938 | "pin-project-lite", |
| 3967 | - "signal-hook-registry", | |
| 3968 | 3939 | "socket2 0.6.4", |
| 3969 | - "tokio-macros", | |
| 3970 | 3940 | "windows-sys 0.61.2", |
| 3971 | 3941 | ] |
| 3972 | 3942 | |
| 3973 | -[[package]] | |
| 3974 | -name = "tokio-macros" | |
| 3975 | -version = "2.7.0" | |
| 3976 | -source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3977 | -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" | |
| 3978 | -dependencies = [ | |
| 3979 | - "proc-macro2", | |
| 3980 | - "quote", | |
| 3981 | - "syn", | |
| 3982 | -] | |
| 3983 | - | |
| 3984 | 3943 | [[package]] |
| 3985 | 3944 | name = "tokio-native-tls" |
| 3986 | 3945 | version = "0.3.1" |
| @@ -4327,19 +4286,6 @@ dependencies = [ | ||
| 4327 | 4286 | "unicode-ident", |
| 4328 | 4287 | ] |
| 4329 | 4288 | |
| 4330 | -[[package]] | |
| 4331 | -name = "wasm-streams" | |
| 4332 | -version = "0.4.2" | |
| 4333 | -source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4334 | -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" | |
| 4335 | -dependencies = [ | |
| 4336 | - "futures-util", | |
| 4337 | - "js-sys", | |
| 4338 | - "wasm-bindgen", | |
| 4339 | - "wasm-bindgen-futures", | |
| 4340 | - "web-sys", | |
| 4341 | -] | |
| 4342 | - | |
| 4343 | 4289 | [[package]] |
| 4344 | 4290 | name = "wayland-backend" |
| 4345 | 4291 | version = "0.3.15" |
| @@ -1395,7 +1395,6 @@ dependencies = [ | |||
| 1395 | "futures-core", | 1395 | "futures-core", |
| 1396 | "futures-io", | 1396 | "futures-io", |
| 1397 | "futures-macro", | 1397 | "futures-macro", |
| 1398 | - "futures-sink", | ||
| 1399 | "futures-task", | 1398 | "futures-task", |
| 1400 | "memchr", | 1399 | "memchr", |
| 1401 | "pin-project-lite", | 1400 | "pin-project-lite", |
| @@ -2234,12 +2233,6 @@ dependencies = [ | |||
| 2234 | "libc", | 2233 | "libc", |
| 2235 | ] | 2234 | ] |
| 2236 | 2235 | ||
| 2237 | -[[package]] | ||
| 2238 | -name = "md5" | ||
| 2239 | -version = "0.7.0" | ||
| 2240 | -source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2241 | -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" | ||
| 2242 | - | ||
| 2243 | [[package]] | 2236 | [[package]] |
| 2244 | name = "memchr" | 2237 | name = "memchr" |
| 2245 | version = "2.8.2" | 2238 | version = "2.8.2" |
| @@ -2301,12 +2294,6 @@ dependencies = [ | |||
| 2301 | "pxfm", | 2294 | "pxfm", |
| 2302 | ] | 2295 | ] |
| 2303 | 2296 | ||
| 2304 | -[[package]] | ||
| 2305 | -name = "mp3-metadata" | ||
| 2306 | -version = "0.3.4" | ||
| 2307 | -source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2308 | -checksum = "e4eaa3a228dc218005d769b88ce3a0310f49152f2029125289e94c49474c9cc4" | ||
| 2309 | - | ||
| 2310 | [[package]] | 2297 | [[package]] |
| 2311 | name = "naga" | 2298 | name = "naga" |
| 2312 | version = "29.0.4" | 2299 | version = "29.0.4" |
| @@ -2884,12 +2871,6 @@ dependencies = [ | |||
| 2884 | "windows-link", | 2871 | "windows-link", |
| 2885 | ] | 2872 | ] |
| 2886 | 2873 | ||
| 2887 | -[[package]] | ||
| 2888 | -name = "pathdiff" | ||
| 2889 | -version = "0.2.3" | ||
| 2890 | -source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 2891 | -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" | ||
| 2892 | - | ||
| 2893 | [[package]] | 2874 | [[package]] |
| 2894 | name = "peniko" | 2875 | name = "peniko" |
| 2895 | version = "0.6.1" | 2876 | version = "0.6.1" |
| @@ -3283,12 +3264,10 @@ dependencies = [ | |||
| 3283 | "system-configuration", | 3264 | "system-configuration", |
| 3284 | "tokio", | 3265 | "tokio", |
| 3285 | "tokio-native-tls", | 3266 | "tokio-native-tls", |
| 3286 | - "tokio-util", | ||
| 3287 | "tower-service", | 3267 | "tower-service", |
| 3288 | "url", | 3268 | "url", |
| 3289 | "wasm-bindgen", | 3269 | "wasm-bindgen", |
| 3290 | "wasm-bindgen-futures", | 3270 | "wasm-bindgen-futures", |
| 3291 | - "wasm-streams", | ||
| 3292 | "web-sys", | 3271 | "web-sys", |
| 3293 | "winreg", | 3272 | "winreg", |
| 3294 | ] | 3273 | ] |
| @@ -3332,16 +3311,11 @@ name = "rss-podcast-dl-egui" | |||
| 3332 | version = "0.2.0" | 3311 | version = "0.2.0" |
| 3333 | dependencies = [ | 3312 | dependencies = [ |
| 3334 | "chrono", | 3313 | "chrono", |
| 3335 | - "diligent-date-parser", | ||
| 3336 | "eframe", | 3314 | "eframe", |
| 3337 | "egui", | 3315 | "egui", |
| 3338 | "env_logger", | 3316 | "env_logger", |
| 3339 | - "futures-util", | ||
| 3340 | "id3", | 3317 | "id3", |
| 3341 | "log", | 3318 | "log", |
| 3342 | - "md5", | ||
| 3343 | - "mp3-metadata", | ||
| 3344 | - "pathdiff", | ||
| 3345 | "reqwest", | 3319 | "reqwest", |
| 3346 | "rfd", | 3320 | "rfd", |
| 3347 | "rss", | 3321 | "rss", |
| @@ -3349,7 +3323,6 @@ dependencies = [ | |||
| 3349 | "serde_json", | 3323 | "serde_json", |
| 3350 | "tempfile", | 3324 | "tempfile", |
| 3351 | "thiserror 1.0.69", | 3325 | "thiserror 1.0.69", |
| 3352 | - "tokio", | ||
| 3353 | "uuid", | 3326 | "uuid", |
| 3354 | ] | 3327 | ] |
| 3355 | 3328 | ||
| @@ -3962,25 +3935,11 @@ dependencies = [ | |||
| 3962 | "bytes", | 3935 | "bytes", |
| 3963 | "libc", | 3936 | "libc", |
| 3964 | "mio", | 3937 | "mio", |
| 3965 | - "parking_lot", | ||
| 3966 | "pin-project-lite", | 3938 | "pin-project-lite", |
| 3967 | - "signal-hook-registry", | ||
| 3968 | "socket2 0.6.4", | 3939 | "socket2 0.6.4", |
| 3969 | - "tokio-macros", | ||
| 3970 | "windows-sys 0.61.2", | 3940 | "windows-sys 0.61.2", |
| 3971 | ] | 3941 | ] |
| 3972 | 3942 | ||
| 3973 | -[[package]] | ||
| 3974 | -name = "tokio-macros" | ||
| 3975 | -version = "2.7.0" | ||
| 3976 | -source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 3977 | -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" | ||
| 3978 | -dependencies = [ | ||
| 3979 | - "proc-macro2", | ||
| 3980 | - "quote", | ||
| 3981 | - "syn", | ||
| 3982 | -] | ||
| 3983 | - | ||
| 3984 | [[package]] | 3943 | [[package]] |
| 3985 | name = "tokio-native-tls" | 3944 | name = "tokio-native-tls" |
| 3986 | version = "0.3.1" | 3945 | version = "0.3.1" |
| @@ -4327,19 +4286,6 @@ dependencies = [ | |||
| 4327 | "unicode-ident", | 4286 | "unicode-ident", |
| 4328 | ] | 4287 | ] |
| 4329 | 4288 | ||
| 4330 | -[[package]] | ||
| 4331 | -name = "wasm-streams" | ||
| 4332 | -version = "0.4.2" | ||
| 4333 | -source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 4334 | -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" | ||
| 4335 | -dependencies = [ | ||
| 4336 | - "futures-util", | ||
| 4337 | - "js-sys", | ||
| 4338 | - "wasm-bindgen", | ||
| 4339 | - "wasm-bindgen-futures", | ||
| 4340 | - "web-sys", | ||
| 4341 | -] | ||
| 4342 | - | ||
| 4343 | [[package]] | 4289 | [[package]] |
| 4344 | name = "wayland-backend" | 4290 | name = "wayland-backend" |
| 4345 | version = "0.3.15" | 4291 | version = "0.3.15" |
modified
Cargo.toml +5 -21 | @@ -21,10 +21,7 @@ serde = { version = "1.0", features = ["derive"] } | ||
| 21 | 21 | serde_json = "1.0" |
| 22 | 22 | |
| 23 | 23 | # HTTP requests |
| 24 | -reqwest = { version = "0.11", features = ["json", "stream", "blocking"] } | |
| 25 | - | |
| 26 | -# Async runtime | |
| 27 | -tokio = { version = "1.0", features = ["full"] } | |
| 24 | +reqwest = { version = "0.11", features = ["blocking"] } | |
| 28 | 25 | |
| 29 | 26 | # RSS parsing |
| 30 | 27 | rss = "2.0" |
| @@ -39,32 +36,19 @@ env_logger = "0.10" | ||
| 39 | 36 | # UUID generation |
| 40 | 37 | uuid = { version = "1.0", features = ["v4", "serde"] } |
| 41 | 38 | |
| 42 | -# Path handling | |
| 43 | -pathdiff = "0.2" | |
| 44 | - | |
| 45 | 39 | # Error handling |
| 46 | 40 | thiserror = "1.0" |
| 47 | 41 | |
| 48 | -# Testing utilities | |
| 49 | -tempfile = "3.0" | |
| 50 | - | |
| 51 | -# Async utilities | |
| 52 | -futures-util = "0.3" | |
| 53 | - | |
| 54 | -# Hashing for filename generation | |
| 55 | -md5 = "0.7" | |
| 56 | - | |
| 57 | 42 | # File dialogs for export/import |
| 58 | 43 | rfd = "0.12" |
| 59 | 44 | |
| 60 | -# Date parsing | |
| 61 | -# The rss crate already includes chrono, but we need additional date parsing | |
| 62 | -diligent-date-parser = "0.1" | |
| 63 | - | |
| 64 | 45 | # Audio metadata parsing |
| 65 | -mp3-metadata = "0.3" | |
| 66 | 46 | id3 = "1.13" |
| 67 | 47 | |
| 48 | +[dev-dependencies] | |
| 49 | +# Testing utilities | |
| 50 | +tempfile = "3.0" | |
| 51 | + | |
| 68 | 52 | # Windows API compatibility for older eframe versions - not needed with eframe 0.35 |
| 69 | 53 | # [target.'cfg(windows)'.dependencies] |
| 70 | 54 | # winapi = { version = "0.3", features = ["winuser", "windef"] } |
| @@ -21,10 +21,7 @@ serde = { version = "1.0", features = ["derive"] } | |||
| 21 | serde_json = "1.0" | 21 | serde_json = "1.0" |
| 22 | 22 | ||
| 23 | # HTTP requests | 23 | # HTTP requests |
| 24 | -reqwest = { version = "0.11", features = ["json", "stream", "blocking"] } | 24 | +reqwest = { version = "0.11", features = ["blocking"] } |
| 25 | - | ||
| 26 | -# Async runtime | ||
| 27 | -tokio = { version = "1.0", features = ["full"] } | ||
| 28 | 25 | ||
| 29 | # RSS parsing | 26 | # RSS parsing |
| 30 | rss = "2.0" | 27 | rss = "2.0" |
| @@ -39,32 +36,19 @@ env_logger = "0.10" | |||
| 39 | # UUID generation | 36 | # UUID generation |
| 40 | uuid = { version = "1.0", features = ["v4", "serde"] } | 37 | uuid = { version = "1.0", features = ["v4", "serde"] } |
| 41 | 38 | ||
| 42 | -# Path handling | ||
| 43 | -pathdiff = "0.2" | ||
| 44 | - | ||
| 45 | # Error handling | 39 | # Error handling |
| 46 | thiserror = "1.0" | 40 | thiserror = "1.0" |
| 47 | 41 | ||
| 48 | -# Testing utilities | ||
| 49 | -tempfile = "3.0" | ||
| 50 | - | ||
| 51 | -# Async utilities | ||
| 52 | -futures-util = "0.3" | ||
| 53 | - | ||
| 54 | -# Hashing for filename generation | ||
| 55 | -md5 = "0.7" | ||
| 56 | - | ||
| 57 | # File dialogs for export/import | 42 | # File dialogs for export/import |
| 58 | rfd = "0.12" | 43 | rfd = "0.12" |
| 59 | 44 | ||
| 60 | -# Date parsing | ||
| 61 | -# The rss crate already includes chrono, but we need additional date parsing | ||
| 62 | -diligent-date-parser = "0.1" | ||
| 63 | - | ||
| 64 | # Audio metadata parsing | 45 | # Audio metadata parsing |
| 65 | -mp3-metadata = "0.3" | ||
| 66 | id3 = "1.13" | 46 | id3 = "1.13" |
| 67 | 47 | ||
| 48 | +[dev-dependencies] | ||
| 49 | +# Testing utilities | ||
| 50 | +tempfile = "3.0" | ||
| 51 | + | ||
| 68 | # Windows API compatibility for older eframe versions - not needed with eframe 0.35 | 52 | # Windows API compatibility for older eframe versions - not needed with eframe 0.35 |
| 69 | # [target.'cfg(windows)'.dependencies] | 53 | # [target.'cfg(windows)'.dependencies] |
| 70 | # winapi = { version = "0.3", features = ["winuser", "windef"] } | 54 | # winapi = { version = "0.3", features = ["winuser", "windef"] } |
modified
src/app.rs +1 -71 | @@ -3,7 +3,7 @@ | ||
| 3 | 3 | //! This module contains the main egui application and state management. |
| 4 | 4 | |
| 5 | 5 | use crate::config::AppState; |
| 6 | -use crate::config::storage::{load_feeds_default, save_feeds_default}; | |
| 6 | +use crate::config::storage::load_feeds_default; | |
| 7 | 7 | use crate::config::Theme; |
| 8 | 8 | use eframe::egui; |
| 9 | 9 | use log::{error, info}; |
| @@ -24,76 +24,6 @@ impl RssPodcastApp { | ||
| 24 | 24 | clear_color: egui::Color32::from_rgb(30, 30, 30), |
| 25 | 25 | } |
| 26 | 26 | } |
| 27 | - | |
| 28 | - /// Saves the current feeds to the default configuration | |
| 29 | - #[allow(dead_code)] | |
| 30 | - fn save_feeds(&self) { | |
| 31 | - if let Err(e) = save_feeds_default(&self.state.feeds) { | |
| 32 | - error!("Failed to save feeds: {}", e); | |
| 33 | - } | |
| 34 | - } | |
| 35 | - | |
| 36 | - /// Synchronizes all configured feeds | |
| 37 | - /// Note: This method is currently duplicated in ui/home.rs for technical reasons | |
| 38 | - #[allow(dead_code)] | |
| 39 | - fn sync_all_feeds(&mut self) { | |
| 40 | - if !self.state.has_feeds() { | |
| 41 | - self.state.set_info("Aucun flux à synchroniser".to_string()); | |
| 42 | - return; | |
| 43 | - } | |
| 44 | - | |
| 45 | - self.state.set_sync_state(true); | |
| 46 | - self.state.set_info("Synchronisation de tous les flux...".to_string()); | |
| 47 | - | |
| 48 | - info!("Starting synchronization of all feeds"); | |
| 49 | - | |
| 50 | - // Perform synchronization | |
| 51 | - let result = crate::rss::sync::sync_all_feeds(&mut self.state.feeds); | |
| 52 | - | |
| 53 | - self.state.set_sync_state(false); | |
| 54 | - | |
| 55 | - match result { | |
| 56 | - Ok(success_count) => { | |
| 57 | - self.state.set_info(format!("{} flux synchronisés avec succès", success_count)); | |
| 58 | - // Save the updated feeds | |
| 59 | - self.save_feeds(); | |
| 60 | - } | |
| 61 | - Err(e) => { | |
| 62 | - self.state.set_error(format!("Erreur de synchronisation: {}", e)); | |
| 63 | - } | |
| 64 | - } | |
| 65 | - } | |
| 66 | - | |
| 67 | - /// Synchronizes a single feed by index | |
| 68 | - /// Note: This method is currently duplicated in ui/feed_list.rs for technical reasons | |
| 69 | - #[allow(dead_code)] | |
| 70 | - fn sync_single_feed(&mut self, index: usize) { | |
| 71 | - if index >= self.state.feeds.len() { | |
| 72 | - self.state.set_error("Index de flux invalide".to_string()); | |
| 73 | - return; | |
| 74 | - } | |
| 75 | - | |
| 76 | - self.state.set_sync_state(true); | |
| 77 | - self.state.set_info(format!("Synchronisation du flux: {}", self.state.feeds[index].name)); | |
| 78 | - | |
| 79 | - info!("Starting synchronization of feed: {}", self.state.feeds[index].name); | |
| 80 | - | |
| 81 | - // Perform synchronization for single feed | |
| 82 | - let result = crate::rss::sync::sync_feed(&mut self.state.feeds[index]); | |
| 83 | - | |
| 84 | - self.state.set_sync_state(false); | |
| 85 | - | |
| 86 | - match result { | |
| 87 | - Ok(episodes) => { | |
| 88 | - self.state.set_info(format!("Flux synchronisé: {} épisodes trouvés", episodes.len())); | |
| 89 | - // Save the updated feeds | |
| 90 | - self.save_feeds(); | |
| 91 | - } | |
| 92 | - Err(e) => { | |
| 93 | - self.state.set_error(format!("Erreur de synchronisation: {}", e)); | |
| 94 | - } | |
| 95 | - } | |
| 96 | - } | |
| 97 | 27 | } |
| 98 | 28 | |
| 99 | 29 | impl Default for RssPodcastApp { |
| @@ -3,7 +3,7 @@ | |||
| 3 | //! This module contains the main egui application and state management. | 3 | //! This module contains the main egui application and state management. |
| 4 | 4 | ||
| 5 | use crate::config::AppState; | 5 | use crate::config::AppState; |
| 6 | -use crate::config::storage::{load_feeds_default, save_feeds_default}; | 6 | +use crate::config::storage::load_feeds_default; |
| 7 | use crate::config::Theme; | 7 | use crate::config::Theme; |
| 8 | use eframe::egui; | 8 | use eframe::egui; |
| 9 | use log::{error, info}; | 9 | use log::{error, info}; |
| @@ -24,76 +24,6 @@ impl RssPodcastApp { | |||
| 24 | clear_color: egui::Color32::from_rgb(30, 30, 30), | 24 | clear_color: egui::Color32::from_rgb(30, 30, 30), |
| 25 | } | 25 | } |
| 26 | } | 26 | } |
| 27 | - | ||
| 28 | - /// Saves the current feeds to the default configuration | ||
| 29 | - #[allow(dead_code)] | ||
| 30 | - fn save_feeds(&self) { | ||
| 31 | - if let Err(e) = save_feeds_default(&self.state.feeds) { | ||
| 32 | - error!("Failed to save feeds: {}", e); | ||
| 33 | - } | ||
| 34 | - } | ||
| 35 | - | ||
| 36 | - /// Synchronizes all configured feeds | ||
| 37 | - /// Note: This method is currently duplicated in ui/home.rs for technical reasons | ||
| 38 | - #[allow(dead_code)] | ||
| 39 | - fn sync_all_feeds(&mut self) { | ||
| 40 | - if !self.state.has_feeds() { | ||
| 41 | - self.state.set_info("Aucun flux à synchroniser".to_string()); | ||
| 42 | - return; | ||
| 43 | - } | ||
| 44 | - | ||
| 45 | - self.state.set_sync_state(true); | ||
| 46 | - self.state.set_info("Synchronisation de tous les flux...".to_string()); | ||
| 47 | - | ||
| 48 | - info!("Starting synchronization of all feeds"); | ||
| 49 | - | ||
| 50 | - // Perform synchronization | ||
| 51 | - let result = crate::rss::sync::sync_all_feeds(&mut self.state.feeds); | ||
| 52 | - | ||
| 53 | - self.state.set_sync_state(false); | ||
| 54 | - | ||
| 55 | - match result { | ||
| 56 | - Ok(success_count) => { | ||
| 57 | - self.state.set_info(format!("{} flux synchronisés avec succès", success_count)); | ||
| 58 | - // Save the updated feeds | ||
| 59 | - self.save_feeds(); | ||
| 60 | - } | ||
| 61 | - Err(e) => { | ||
| 62 | - self.state.set_error(format!("Erreur de synchronisation: {}", e)); | ||
| 63 | - } | ||
| 64 | - } | ||
| 65 | - } | ||
| 66 | - | ||
| 67 | - /// Synchronizes a single feed by index | ||
| 68 | - /// Note: This method is currently duplicated in ui/feed_list.rs for technical reasons | ||
| 69 | - #[allow(dead_code)] | ||
| 70 | - fn sync_single_feed(&mut self, index: usize) { | ||
| 71 | - if index >= self.state.feeds.len() { | ||
| 72 | - self.state.set_error("Index de flux invalide".to_string()); | ||
| 73 | - return; | ||
| 74 | - } | ||
| 75 | - | ||
| 76 | - self.state.set_sync_state(true); | ||
| 77 | - self.state.set_info(format!("Synchronisation du flux: {}", self.state.feeds[index].name)); | ||
| 78 | - | ||
| 79 | - info!("Starting synchronization of feed: {}", self.state.feeds[index].name); | ||
| 80 | - | ||
| 81 | - // Perform synchronization for single feed | ||
| 82 | - let result = crate::rss::sync::sync_feed(&mut self.state.feeds[index]); | ||
| 83 | - | ||
| 84 | - self.state.set_sync_state(false); | ||
| 85 | - | ||
| 86 | - match result { | ||
| 87 | - Ok(episodes) => { | ||
| 88 | - self.state.set_info(format!("Flux synchronisé: {} épisodes trouvés", episodes.len())); | ||
| 89 | - // Save the updated feeds | ||
| 90 | - self.save_feeds(); | ||
| 91 | - } | ||
| 92 | - Err(e) => { | ||
| 93 | - self.state.set_error(format!("Erreur de synchronisation: {}", e)); | ||
| 94 | - } | ||
| 95 | - } | ||
| 96 | - } | ||
| 97 | } | 27 | } |
| 98 | 28 | ||
| 99 | impl Default for RssPodcastApp { | 29 | impl Default for RssPodcastApp { |
modified
src/config/app_state.rs +0 -33 | @@ -301,16 +301,6 @@ impl AppState { | ||
| 301 | 301 | } |
| 302 | 302 | } |
| 303 | 303 | |
| 304 | - /// Sets an error message | |
| 305 | - pub fn set_error(&mut self, message: impl Into<String>) { | |
| 306 | - self.error_message = Some(message.into()); | |
| 307 | - } | |
| 308 | - | |
| 309 | - /// Sets an info message | |
| 310 | - pub fn set_info(&mut self, message: impl Into<String>) { | |
| 311 | - self.info_message = Some(message.into()); | |
| 312 | - } | |
| 313 | - | |
| 314 | 304 | /// Clears all messages |
| 315 | 305 | #[allow(dead_code)] |
| 316 | 306 | pub fn clear_messages(&mut self) { |
| @@ -352,11 +342,6 @@ impl AppState { | ||
| 352 | 342 | self.sync_in_progress |
| 353 | 343 | } |
| 354 | 344 | |
| 355 | - /// Sets the synchronization state | |
| 356 | - pub fn set_sync_state(&mut self, syncing: bool) { | |
| 357 | - self.sync_in_progress = syncing; | |
| 358 | - } | |
| 359 | - | |
| 360 | 345 | /// Updates the download progress for a feed |
| 361 | 346 | #[allow(dead_code)] |
| 362 | 347 | pub fn update_download_progress(&mut self, feed_id: &str, progress: f32) { |
| @@ -647,24 +632,6 @@ mod tests { | ||
| 647 | 632 | assert_eq!(app_state.enabled_feed_count(), 2); |
| 648 | 633 | } |
| 649 | 634 | |
| 650 | - #[test] | |
| 651 | - fn test_messages() { | |
| 652 | - let mut app_state = AppState::new("test/config.json"); | |
| 653 | - | |
| 654 | - assert!(app_state.error_message.is_none()); | |
| 655 | - assert!(app_state.info_message.is_none()); | |
| 656 | - | |
| 657 | - app_state.set_error("Test error"); | |
| 658 | - assert_eq!(app_state.error_message, Some("Test error".to_string())); | |
| 659 | - | |
| 660 | - app_state.set_info("Test info"); | |
| 661 | - assert_eq!(app_state.info_message, Some("Test info".to_string())); | |
| 662 | - | |
| 663 | - app_state.clear_messages(); | |
| 664 | - assert!(app_state.error_message.is_none()); | |
| 665 | - assert!(app_state.info_message.is_none()); | |
| 666 | - } | |
| 667 | - | |
| 668 | 635 | #[test] |
| 669 | 636 | fn test_download_progress() { |
| 670 | 637 | let mut app_state = AppState::new("test/config.json"); |
| @@ -301,16 +301,6 @@ impl AppState { | |||
| 301 | } | 301 | } |
| 302 | } | 302 | } |
| 303 | 303 | ||
| 304 | - /// Sets an error message | ||
| 305 | - pub fn set_error(&mut self, message: impl Into<String>) { | ||
| 306 | - self.error_message = Some(message.into()); | ||
| 307 | - } | ||
| 308 | - | ||
| 309 | - /// Sets an info message | ||
| 310 | - pub fn set_info(&mut self, message: impl Into<String>) { | ||
| 311 | - self.info_message = Some(message.into()); | ||
| 312 | - } | ||
| 313 | - | ||
| 314 | /// Clears all messages | 304 | /// Clears all messages |
| 315 | #[allow(dead_code)] | 305 | #[allow(dead_code)] |
| 316 | pub fn clear_messages(&mut self) { | 306 | pub fn clear_messages(&mut self) { |
| @@ -352,11 +342,6 @@ impl AppState { | |||
| 352 | self.sync_in_progress | 342 | self.sync_in_progress |
| 353 | } | 343 | } |
| 354 | 344 | ||
| 355 | - /// Sets the synchronization state | ||
| 356 | - pub fn set_sync_state(&mut self, syncing: bool) { | ||
| 357 | - self.sync_in_progress = syncing; | ||
| 358 | - } | ||
| 359 | - | ||
| 360 | /// Updates the download progress for a feed | 345 | /// Updates the download progress for a feed |
| 361 | #[allow(dead_code)] | 346 | #[allow(dead_code)] |
| 362 | pub fn update_download_progress(&mut self, feed_id: &str, progress: f32) { | 347 | pub fn update_download_progress(&mut self, feed_id: &str, progress: f32) { |
| @@ -647,24 +632,6 @@ mod tests { | |||
| 647 | assert_eq!(app_state.enabled_feed_count(), 2); | 632 | assert_eq!(app_state.enabled_feed_count(), 2); |
| 648 | } | 633 | } |
| 649 | 634 | ||
| 650 | - #[test] | ||
| 651 | - fn test_messages() { | ||
| 652 | - let mut app_state = AppState::new("test/config.json"); | ||
| 653 | - | ||
| 654 | - assert!(app_state.error_message.is_none()); | ||
| 655 | - assert!(app_state.info_message.is_none()); | ||
| 656 | - | ||
| 657 | - app_state.set_error("Test error"); | ||
| 658 | - assert_eq!(app_state.error_message, Some("Test error".to_string())); | ||
| 659 | - | ||
| 660 | - app_state.set_info("Test info"); | ||
| 661 | - assert_eq!(app_state.info_message, Some("Test info".to_string())); | ||
| 662 | - | ||
| 663 | - app_state.clear_messages(); | ||
| 664 | - assert!(app_state.error_message.is_none()); | ||
| 665 | - assert!(app_state.info_message.is_none()); | ||
| 666 | - } | ||
| 667 | - | ||
| 668 | #[test] | 635 | #[test] |
| 669 | fn test_download_progress() { | 636 | fn test_download_progress() { |
| 670 | let mut app_state = AppState::new("test/config.json"); | 637 | let mut app_state = AppState::new("test/config.json"); |
modified
src/rss/mod.rs +0 -4 | @@ -7,7 +7,6 @@ pub mod audio_metadata; | ||
| 7 | 7 | pub mod downloader; |
| 8 | 8 | pub mod episode_extractor; |
| 9 | 9 | pub mod parser; |
| 10 | -pub mod sync; | |
| 11 | 10 | |
| 12 | 11 | // Re-export main types for convenience |
| 13 | 12 | // Note: Some exports may not be directly used in the current codebase but are |
| @@ -27,6 +26,3 @@ pub use episode_extractor::{ExtractionError, extract_episodes, extract_episodes_ | ||
| 27 | 26 | #[allow(dead_code)] |
| 28 | 27 | #[allow(unused_imports)] |
| 29 | 28 | pub use parser::{FeedClient, ParserError, RssFeed, fetch_feed, parse_rss_content}; |
| 30 | -#[allow(dead_code)] | |
| 31 | -#[allow(unused_imports)] | |
| 32 | -pub use sync::{download_new_episodes, resolve_filename_conflict, sync_all_feeds, sync_feed, SyncError}; | |
| @@ -7,7 +7,6 @@ pub mod audio_metadata; | |||
| 7 | pub mod downloader; | 7 | pub mod downloader; |
| 8 | pub mod episode_extractor; | 8 | pub mod episode_extractor; |
| 9 | pub mod parser; | 9 | pub mod parser; |
| 10 | -pub mod sync; | ||
| 11 | 10 | ||
| 12 | // Re-export main types for convenience | 11 | // Re-export main types for convenience |
| 13 | // Note: Some exports may not be directly used in the current codebase but are | 12 | // Note: Some exports may not be directly used in the current codebase but are |
| @@ -27,6 +26,3 @@ pub use episode_extractor::{ExtractionError, extract_episodes, extract_episodes_ | |||
| 27 | #[allow(dead_code)] | 26 | #[allow(dead_code)] |
| 28 | #[allow(unused_imports)] | 27 | #[allow(unused_imports)] |
| 29 | pub use parser::{FeedClient, ParserError, RssFeed, fetch_feed, parse_rss_content}; | 28 | pub use parser::{FeedClient, ParserError, RssFeed, fetch_feed, parse_rss_content}; |
| 30 | -#[allow(dead_code)] | ||
| 31 | -#[allow(unused_imports)] | ||
| 32 | -pub use sync::{download_new_episodes, resolve_filename_conflict, sync_all_feeds, sync_feed, SyncError}; | ||
deleted
src/rss/sync.rs +0 -217 | deleted file mode 100644 | ||
| @@ -1,217 +0,0 @@ | ||
| 1 | -//! Synchronization module for RSS Podcast Downloader | |
| 2 | -//! | |
| 3 | -//! This module handles the synchronization of RSS feeds, including fetching, | |
| 4 | -//! parsing, episode extraction, and downloading new episodes. | |
| 5 | - | |
| 6 | -use crate::config::{Episode, Feed}; | |
| 7 | -use crate::rss::downloader::download_episode; | |
| 8 | -use crate::rss::episode_extractor::extract_episodes; | |
| 9 | -use crate::rss::parser::{fetch_feed, parse_rss_content}; | |
| 10 | -use chrono::Utc; | |
| 11 | -use log::{error, info, warn}; | |
| 12 | -use std::path::Path; | |
| 13 | - | |
| 14 | -/// Error types for synchronization operations | |
| 15 | -#[derive(Debug)] | |
| 16 | -pub enum SyncError { | |
| 17 | - /// Failed to fetch the RSS feed | |
| 18 | - FetchError(String), | |
| 19 | - /// Failed to parse the RSS content | |
| 20 | - ParseError(String), | |
| 21 | - /// Failed to extract episodes from the feed | |
| 22 | - ExtractionError(String), | |
| 23 | - /// Failed to download an episode | |
| 24 | - DownloadError(String), | |
| 25 | - /// No episodes found in the feed | |
| 26 | - NoEpisodes, | |
| 27 | - /// Feed is disabled | |
| 28 | - FeedDisabled, | |
| 29 | - /// Invalid URL | |
| 30 | - InvalidUrl(String), | |
| 31 | - /// Invalid download path | |
| 32 | - InvalidPath(String), | |
| 33 | - /// Episode already downloaded | |
| 34 | - #[allow(dead_code)] | |
| 35 | - EpisodeAlreadyExists(String), | |
| 36 | -} | |
| 37 | - | |
| 38 | -impl std::fmt::Display for SyncError { | |
| 39 | - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 40 | - match self { | |
| 41 | - SyncError::FetchError(msg) => write!(f, "Fetch error: {}", msg), | |
| 42 | - SyncError::ParseError(msg) => write!(f, "Parse error: {}", msg), | |
| 43 | - SyncError::ExtractionError(msg) => write!(f, "Extraction error: {}", msg), | |
| 44 | - SyncError::DownloadError(msg) => write!(f, "Download error: {}", msg), | |
| 45 | - SyncError::NoEpisodes => write!(f, "No episodes found in feed"), | |
| 46 | - SyncError::FeedDisabled => write!(f, "Feed is disabled"), | |
| 47 | - SyncError::InvalidUrl(msg) => write!(f, "Invalid URL: {}", msg), | |
| 48 | - SyncError::InvalidPath(msg) => write!(f, "Invalid path: {}", msg), | |
| 49 | - SyncError::EpisodeAlreadyExists(msg) => write!(f, "Episode already exists: {}", msg), | |
| 50 | - } | |
| 51 | - } | |
| 52 | -} | |
| 53 | - | |
| 54 | -impl std::error::Error for SyncError {} | |
| 55 | - | |
| 56 | -/// Synchronizes a single feed: fetches, parses, extracts episodes, and downloads new ones | |
| 57 | -pub fn sync_feed(feed: &mut Feed) -> Result<Vec<Episode>, SyncError> { | |
| 58 | - if !feed.enabled { | |
| 59 | - return Err(SyncError::FeedDisabled); | |
| 60 | - } | |
| 61 | - | |
| 62 | - // Validate URL | |
| 63 | - if feed.url.is_empty() { | |
| 64 | - return Err(SyncError::InvalidUrl("URL cannot be empty".to_string())); | |
| 65 | - } | |
| 66 | - | |
| 67 | - // Validate download path | |
| 68 | - if feed.download_path.to_string_lossy().is_empty() { | |
| 69 | - return Err(SyncError::InvalidPath("Download path cannot be empty".to_string())); | |
| 70 | - } | |
| 71 | - | |
| 72 | - info!("Starting synchronization for feed: {} ({})", feed.name, feed.url); | |
| 73 | - | |
| 74 | - // Fetch the feed content | |
| 75 | - let content = fetch_feed(&feed.url) | |
| 76 | - .map_err(|e| { | |
| 77 | - error!("Failed to fetch feed {}: {}", feed.url, e); | |
| 78 | - SyncError::FetchError(e.to_string()) | |
| 79 | - })?; | |
| 80 | - | |
| 81 | - // Parse the RSS content | |
| 82 | - let channel = parse_rss_content(&content) | |
| 83 | - .map_err(|e| { | |
| 84 | - error!("Failed to parse feed {}: {}", feed.url, e); | |
| 85 | - SyncError::ParseError(e.to_string()) | |
| 86 | - })?; | |
| 87 | - | |
| 88 | - // Extract episodes from the channel | |
| 89 | - let episodes = extract_episodes(&channel) | |
| 90 | - .map_err(|e| { | |
| 91 | - error!("Failed to extract episodes from feed {}: {}", feed.url, e); | |
| 92 | - SyncError::ExtractionError(e.to_string()) | |
| 93 | - })?; | |
| 94 | - | |
| 95 | - if episodes.is_empty() { | |
| 96 | - warn!("No episodes found in feed: {}", feed.url); | |
| 97 | - return Err(SyncError::NoEpisodes); | |
| 98 | - } | |
| 99 | - | |
| 100 | - info!("Found {} episodes in feed: {}", episodes.len(), feed.url); | |
| 101 | - | |
| 102 | - // Update feed metadata | |
| 103 | - feed.last_checked = Some(Utc::now()); | |
| 104 | - if let Some(latest) = episodes.iter().max_by(|a, b| a.pub_date.cmp(&b.pub_date)) { | |
| 105 | - feed.last_episode_date = Some(latest.pub_date); | |
| 106 | - } | |
| 107 | - | |
| 108 | - // Download new episodes | |
| 109 | - let downloaded_count = download_new_episodes(feed, &episodes)?; | |
| 110 | - info!("Downloaded {} new episodes for feed: {}", downloaded_count, feed.name); | |
| 111 | - | |
| 112 | - Ok(episodes) | |
| 113 | -} | |
| 114 | - | |
| 115 | -/// Downloads new episodes that haven't been downloaded yet | |
| 116 | -pub fn download_new_episodes(feed: &mut Feed, episodes: &[Episode]) -> Result<usize, SyncError> { | |
| 117 | - let mut downloaded_count = 0; | |
| 118 | - | |
| 119 | - for episode in episodes { | |
| 120 | - // Skip episodes that are already downloaded | |
| 121 | - if is_episode_downloaded(feed, episode) { | |
| 122 | - info!("Episode already downloaded: {}", episode.title); | |
| 123 | - continue; | |
| 124 | - } | |
| 125 | - | |
| 126 | - // Download the episode | |
| 127 | - match download_episode(episode, &feed.download_path, Box::new(|_progress| {})) { | |
| 128 | - Ok(_) => { | |
| 129 | - info!("Successfully downloaded episode: {}", episode.title); | |
| 130 | - downloaded_count += 1; | |
| 131 | - | |
| 132 | - // Update feed download statistics | |
| 133 | - feed.downloaded_episodes_count += 1; | |
| 134 | - feed.last_downloaded_episode_name = Some(episode.title.clone()); | |
| 135 | - feed.last_downloaded_episode_date = Some(Utc::now()); | |
| 136 | - } | |
| 137 | - Err(e) => { | |
| 138 | - error!("Failed to download episode {}: {}", episode.title, e); | |
| 139 | - return Err(SyncError::DownloadError(e.to_string())); | |
| 140 | - } | |
| 141 | - } | |
| 142 | - } | |
| 143 | - | |
| 144 | - Ok(downloaded_count) | |
| 145 | -} | |
| 146 | - | |
| 147 | -/// Checks if an episode has already been downloaded | |
| 148 | -fn is_episode_downloaded(feed: &Feed, episode: &Episode) -> bool { | |
| 149 | - !crate::rss::downloader::needs_download(episode, std::path::Path::new(&feed.download_path)) | |
| 150 | -} | |
| 151 | - | |
| 152 | -/// Synchronizes all feeds in the collection | |
| 153 | -pub fn sync_all_feeds(feeds: &mut [Feed]) -> Result<usize, SyncError> { | |
| 154 | - info!("Starting synchronization of all {} feeds", feeds.len()); | |
| 155 | - | |
| 156 | - let mut successful_feeds = 0; | |
| 157 | - let mut failed_feeds = 0; | |
| 158 | - | |
| 159 | - for feed in feeds.iter_mut() { | |
| 160 | - match sync_feed(feed) { | |
| 161 | - Ok(_) => { | |
| 162 | - successful_feeds += 1; | |
| 163 | - } | |
| 164 | - Err(e) => { | |
| 165 | - error!("Failed to sync feed {}: {}", feed.name, e); | |
| 166 | - failed_feeds += 1; | |
| 167 | - } | |
| 168 | - } | |
| 169 | - } | |
| 170 | - | |
| 171 | - info!( | |
| 172 | - "Synchronization complete: {} feeds succeeded, {} feeds failed", | |
| 173 | - successful_feeds, failed_feeds | |
| 174 | - ); | |
| 175 | - | |
| 176 | - Ok(successful_feeds) | |
| 177 | -} | |
| 178 | - | |
| 179 | -/// Resolves filename conflicts by adding a numeric suffix | |
| 180 | -#[allow(dead_code)] | |
| 181 | -pub fn resolve_filename_conflict(original_path: &Path) -> std::path::PathBuf { | |
| 182 | - let mut path = original_path.to_path_buf(); | |
| 183 | - let mut counter = 1; | |
| 184 | - | |
| 185 | - while path.exists() { | |
| 186 | - let stem = path.file_stem().unwrap_or_default().to_string_lossy(); | |
| 187 | - let extension = path.extension().map(|ext| format!(".{}", ext.to_string_lossy())).unwrap_or_default(); | |
| 188 | - let new_stem = format!("{}_{}", stem, counter); | |
| 189 | - let new_filename = format!("{}{}", new_stem, extension); | |
| 190 | - path.set_file_name(new_filename); | |
| 191 | - counter += 1; | |
| 192 | - } | |
| 193 | - | |
| 194 | - path | |
| 195 | -} | |
| 196 | - | |
| 197 | -#[cfg(test)] | |
| 198 | -mod tests { | |
| 199 | - use super::*; | |
| 200 | - | |
| 201 | - #[test] | |
| 202 | - fn test_resolve_filename_conflict() { | |
| 203 | - use tempfile::tempdir; | |
| 204 | - | |
| 205 | - // Create a temporary directory and a test file | |
| 206 | - let temp_dir = tempdir().unwrap(); | |
| 207 | - let existing_file = temp_dir.path().join("test.mp3"); | |
| 208 | - std::fs::File::create(&existing_file).unwrap(); | |
| 209 | - | |
| 210 | - // Try to resolve conflict for the same path | |
| 211 | - let resolved = resolve_filename_conflict(&existing_file); | |
| 212 | - | |
| 213 | - // Should have added suffix since file exists | |
| 214 | - assert!(resolved.to_string_lossy().contains("_1")); | |
| 215 | - assert_ne!(resolved, existing_file); | |
| 216 | - } | |
| 217 | -} | |
| deleted file mode 100644 | |||
| @@ -1,217 +0,0 @@ | |||
| 1 | -//! Synchronization module for RSS Podcast Downloader | ||
| 2 | -//! | ||
| 3 | -//! This module handles the synchronization of RSS feeds, including fetching, | ||
| 4 | -//! parsing, episode extraction, and downloading new episodes. | ||
| 5 | - | ||
| 6 | -use crate::config::{Episode, Feed}; | ||
| 7 | -use crate::rss::downloader::download_episode; | ||
| 8 | -use crate::rss::episode_extractor::extract_episodes; | ||
| 9 | -use crate::rss::parser::{fetch_feed, parse_rss_content}; | ||
| 10 | -use chrono::Utc; | ||
| 11 | -use log::{error, info, warn}; | ||
| 12 | -use std::path::Path; | ||
| 13 | - | ||
| 14 | -/// Error types for synchronization operations | ||
| 15 | -#[derive(Debug)] | ||
| 16 | -pub enum SyncError { | ||
| 17 | - /// Failed to fetch the RSS feed | ||
| 18 | - FetchError(String), | ||
| 19 | - /// Failed to parse the RSS content | ||
| 20 | - ParseError(String), | ||
| 21 | - /// Failed to extract episodes from the feed | ||
| 22 | - ExtractionError(String), | ||
| 23 | - /// Failed to download an episode | ||
| 24 | - DownloadError(String), | ||
| 25 | - /// No episodes found in the feed | ||
| 26 | - NoEpisodes, | ||
| 27 | - /// Feed is disabled | ||
| 28 | - FeedDisabled, | ||
| 29 | - /// Invalid URL | ||
| 30 | - InvalidUrl(String), | ||
| 31 | - /// Invalid download path | ||
| 32 | - InvalidPath(String), | ||
| 33 | - /// Episode already downloaded | ||
| 34 | - #[allow(dead_code)] | ||
| 35 | - EpisodeAlreadyExists(String), | ||
| 36 | -} | ||
| 37 | - | ||
| 38 | -impl std::fmt::Display for SyncError { | ||
| 39 | - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 40 | - match self { | ||
| 41 | - SyncError::FetchError(msg) => write!(f, "Fetch error: {}", msg), | ||
| 42 | - SyncError::ParseError(msg) => write!(f, "Parse error: {}", msg), | ||
| 43 | - SyncError::ExtractionError(msg) => write!(f, "Extraction error: {}", msg), | ||
| 44 | - SyncError::DownloadError(msg) => write!(f, "Download error: {}", msg), | ||
| 45 | - SyncError::NoEpisodes => write!(f, "No episodes found in feed"), | ||
| 46 | - SyncError::FeedDisabled => write!(f, "Feed is disabled"), | ||
| 47 | - SyncError::InvalidUrl(msg) => write!(f, "Invalid URL: {}", msg), | ||
| 48 | - SyncError::InvalidPath(msg) => write!(f, "Invalid path: {}", msg), | ||
| 49 | - SyncError::EpisodeAlreadyExists(msg) => write!(f, "Episode already exists: {}", msg), | ||
| 50 | - } | ||
| 51 | - } | ||
| 52 | -} | ||
| 53 | - | ||
| 54 | -impl std::error::Error for SyncError {} | ||
| 55 | - | ||
| 56 | -/// Synchronizes a single feed: fetches, parses, extracts episodes, and downloads new ones | ||
| 57 | -pub fn sync_feed(feed: &mut Feed) -> Result<Vec<Episode>, SyncError> { | ||
| 58 | - if !feed.enabled { | ||
| 59 | - return Err(SyncError::FeedDisabled); | ||
| 60 | - } | ||
| 61 | - | ||
| 62 | - // Validate URL | ||
| 63 | - if feed.url.is_empty() { | ||
| 64 | - return Err(SyncError::InvalidUrl("URL cannot be empty".to_string())); | ||
| 65 | - } | ||
| 66 | - | ||
| 67 | - // Validate download path | ||
| 68 | - if feed.download_path.to_string_lossy().is_empty() { | ||
| 69 | - return Err(SyncError::InvalidPath("Download path cannot be empty".to_string())); | ||
| 70 | - } | ||
| 71 | - | ||
| 72 | - info!("Starting synchronization for feed: {} ({})", feed.name, feed.url); | ||
| 73 | - | ||
| 74 | - // Fetch the feed content | ||
| 75 | - let content = fetch_feed(&feed.url) | ||
| 76 | - .map_err(|e| { | ||
| 77 | - error!("Failed to fetch feed {}: {}", feed.url, e); | ||
| 78 | - SyncError::FetchError(e.to_string()) | ||
| 79 | - })?; | ||
| 80 | - | ||
| 81 | - // Parse the RSS content | ||
| 82 | - let channel = parse_rss_content(&content) | ||
| 83 | - .map_err(|e| { | ||
| 84 | - error!("Failed to parse feed {}: {}", feed.url, e); | ||
| 85 | - SyncError::ParseError(e.to_string()) | ||
| 86 | - })?; | ||
| 87 | - | ||
| 88 | - // Extract episodes from the channel | ||
| 89 | - let episodes = extract_episodes(&channel) | ||
| 90 | - .map_err(|e| { | ||
| 91 | - error!("Failed to extract episodes from feed {}: {}", feed.url, e); | ||
| 92 | - SyncError::ExtractionError(e.to_string()) | ||
| 93 | - })?; | ||
| 94 | - | ||
| 95 | - if episodes.is_empty() { | ||
| 96 | - warn!("No episodes found in feed: {}", feed.url); | ||
| 97 | - return Err(SyncError::NoEpisodes); | ||
| 98 | - } | ||
| 99 | - | ||
| 100 | - info!("Found {} episodes in feed: {}", episodes.len(), feed.url); | ||
| 101 | - | ||
| 102 | - // Update feed metadata | ||
| 103 | - feed.last_checked = Some(Utc::now()); | ||
| 104 | - if let Some(latest) = episodes.iter().max_by(|a, b| a.pub_date.cmp(&b.pub_date)) { | ||
| 105 | - feed.last_episode_date = Some(latest.pub_date); | ||
| 106 | - } | ||
| 107 | - | ||
| 108 | - // Download new episodes | ||
| 109 | - let downloaded_count = download_new_episodes(feed, &episodes)?; | ||
| 110 | - info!("Downloaded {} new episodes for feed: {}", downloaded_count, feed.name); | ||
| 111 | - | ||
| 112 | - Ok(episodes) | ||
| 113 | -} | ||
| 114 | - | ||
| 115 | -/// Downloads new episodes that haven't been downloaded yet | ||
| 116 | -pub fn download_new_episodes(feed: &mut Feed, episodes: &[Episode]) -> Result<usize, SyncError> { | ||
| 117 | - let mut downloaded_count = 0; | ||
| 118 | - | ||
| 119 | - for episode in episodes { | ||
| 120 | - // Skip episodes that are already downloaded | ||
| 121 | - if is_episode_downloaded(feed, episode) { | ||
| 122 | - info!("Episode already downloaded: {}", episode.title); | ||
| 123 | - continue; | ||
| 124 | - } | ||
| 125 | - | ||
| 126 | - // Download the episode | ||
| 127 | - match download_episode(episode, &feed.download_path, Box::new(|_progress| {})) { | ||
| 128 | - Ok(_) => { | ||
| 129 | - info!("Successfully downloaded episode: {}", episode.title); | ||
| 130 | - downloaded_count += 1; | ||
| 131 | - | ||
| 132 | - // Update feed download statistics | ||
| 133 | - feed.downloaded_episodes_count += 1; | ||
| 134 | - feed.last_downloaded_episode_name = Some(episode.title.clone()); | ||
| 135 | - feed.last_downloaded_episode_date = Some(Utc::now()); | ||
| 136 | - } | ||
| 137 | - Err(e) => { | ||
| 138 | - error!("Failed to download episode {}: {}", episode.title, e); | ||
| 139 | - return Err(SyncError::DownloadError(e.to_string())); | ||
| 140 | - } | ||
| 141 | - } | ||
| 142 | - } | ||
| 143 | - | ||
| 144 | - Ok(downloaded_count) | ||
| 145 | -} | ||
| 146 | - | ||
| 147 | -/// Checks if an episode has already been downloaded | ||
| 148 | -fn is_episode_downloaded(feed: &Feed, episode: &Episode) -> bool { | ||
| 149 | - !crate::rss::downloader::needs_download(episode, std::path::Path::new(&feed.download_path)) | ||
| 150 | -} | ||
| 151 | - | ||
| 152 | -/// Synchronizes all feeds in the collection | ||
| 153 | -pub fn sync_all_feeds(feeds: &mut [Feed]) -> Result<usize, SyncError> { | ||
| 154 | - info!("Starting synchronization of all {} feeds", feeds.len()); | ||
| 155 | - | ||
| 156 | - let mut successful_feeds = 0; | ||
| 157 | - let mut failed_feeds = 0; | ||
| 158 | - | ||
| 159 | - for feed in feeds.iter_mut() { | ||
| 160 | - match sync_feed(feed) { | ||
| 161 | - Ok(_) => { | ||
| 162 | - successful_feeds += 1; | ||
| 163 | - } | ||
| 164 | - Err(e) => { | ||
| 165 | - error!("Failed to sync feed {}: {}", feed.name, e); | ||
| 166 | - failed_feeds += 1; | ||
| 167 | - } | ||
| 168 | - } | ||
| 169 | - } | ||
| 170 | - | ||
| 171 | - info!( | ||
| 172 | - "Synchronization complete: {} feeds succeeded, {} feeds failed", | ||
| 173 | - successful_feeds, failed_feeds | ||
| 174 | - ); | ||
| 175 | - | ||
| 176 | - Ok(successful_feeds) | ||
| 177 | -} | ||
| 178 | - | ||
| 179 | -/// Resolves filename conflicts by adding a numeric suffix | ||
| 180 | -#[allow(dead_code)] | ||
| 181 | -pub fn resolve_filename_conflict(original_path: &Path) -> std::path::PathBuf { | ||
| 182 | - let mut path = original_path.to_path_buf(); | ||
| 183 | - let mut counter = 1; | ||
| 184 | - | ||
| 185 | - while path.exists() { | ||
| 186 | - let stem = path.file_stem().unwrap_or_default().to_string_lossy(); | ||
| 187 | - let extension = path.extension().map(|ext| format!(".{}", ext.to_string_lossy())).unwrap_or_default(); | ||
| 188 | - let new_stem = format!("{}_{}", stem, counter); | ||
| 189 | - let new_filename = format!("{}{}", new_stem, extension); | ||
| 190 | - path.set_file_name(new_filename); | ||
| 191 | - counter += 1; | ||
| 192 | - } | ||
| 193 | - | ||
| 194 | - path | ||
| 195 | -} | ||
| 196 | - | ||
| 197 | -#[cfg(test)] | ||
| 198 | -mod tests { | ||
| 199 | - use super::*; | ||
| 200 | - | ||
| 201 | - #[test] | ||
| 202 | - fn test_resolve_filename_conflict() { | ||
| 203 | - use tempfile::tempdir; | ||
| 204 | - | ||
| 205 | - // Create a temporary directory and a test file | ||
| 206 | - let temp_dir = tempdir().unwrap(); | ||
| 207 | - let existing_file = temp_dir.path().join("test.mp3"); | ||
| 208 | - std::fs::File::create(&existing_file).unwrap(); | ||
| 209 | - | ||
| 210 | - // Try to resolve conflict for the same path | ||
| 211 | - let resolved = resolve_filename_conflict(&existing_file); | ||
| 212 | - | ||
| 213 | - // Should have added suffix since file exists | ||
| 214 | - assert!(resolved.to_string_lossy().contains("_1")); | ||
| 215 | - assert_ne!(resolved, existing_file); | ||
| 216 | - } | ||
| 217 | -} | ||