Skip to content

Commit af9785f

Browse files
committed
Implement various issue
#12 #15 and #16
1 parent 298fbac commit af9785f

7 files changed

Lines changed: 524 additions & 36 deletions

File tree

src/config.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,8 @@ impl Config {
223223
pub fn load_or_create() -> Result<Self, ConfigError> {
224224
let path = Self::config_path()?;
225225
if !path.exists() {
226-
let config = Self::default();
227-
config.save()?;
228-
return Ok(config);
226+
// Return default config without saving - wizard will save after user input
227+
return Ok(Self::default());
229228
}
230229
Self::load_from(&path)
231230
}

src/history.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ impl WatchHistory {
163163
mod tests {
164164
use super::*;
165165

166+
const SECONDS_PER_DAY: u64 = 86400; // 24 * 60 * 60
167+
166168
#[test]
167169
fn test_make_key_with_tmdb_id() {
168170
let key = WatchHistory::make_key(Some(12345), "ignored_filename.mkv");
@@ -335,7 +337,7 @@ mod tests {
335337
"recent".to_string(),
336338
WatchEntry {
337339
progress_percent: 50.0,
338-
last_watched: now - (1 * 24 * 60 * 60), // 1 day ago
340+
last_watched: now - SECONDS_PER_DAY, // 1 day ago
339341
title: "Recent".to_string(),
340342
},
341343
);
@@ -345,7 +347,7 @@ mod tests {
345347
"old".to_string(),
346348
WatchEntry {
347349
progress_percent: 50.0,
348-
last_watched: now - (31 * 24 * 60 * 60), // 31 days ago
350+
last_watched: now - (31 * SECONDS_PER_DAY), // 31 days ago
349351
title: "Old".to_string(),
350352
},
351353
);

src/providers/trakt.rs

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use super::{ContentProvider, DiscoveryItem, ProviderError, TokenResponse};
99
use async_trait::async_trait;
1010
use reqwest::Client;
11-
use serde::Deserialize;
11+
use serde::{Deserialize, Serialize};
1212
use tracing::debug;
1313

1414
const TRAKT_API_URL: &str = "https://api.trakt.tv";
@@ -148,6 +148,60 @@ fn movie_to_discovery(movie: &TraktMovie) -> Option<DiscoveryItem> {
148148
})
149149
}
150150

151+
// ========== Rating and History API Types ==========
152+
153+
/// Request body for adding items to history (marking as watched)
154+
#[derive(Debug, Clone, Serialize)]
155+
struct HistoryRequest {
156+
#[serde(skip_serializing_if = "Option::is_none")]
157+
movies: Option<Vec<HistoryMovie>>,
158+
#[serde(skip_serializing_if = "Option::is_none")]
159+
episodes: Option<Vec<HistoryEpisode>>,
160+
}
161+
162+
#[derive(Debug, Clone, Serialize)]
163+
struct HistoryMovie {
164+
title: String,
165+
year: u16,
166+
ids: HistoryIds,
167+
}
168+
169+
// TODO: Add support for marking TV episodes as watched
170+
#[derive(Debug, Clone, Serialize)]
171+
struct HistoryEpisode {
172+
ids: HistoryIds,
173+
}
174+
175+
#[derive(Debug, Clone, Serialize)]
176+
struct HistoryIds {
177+
#[serde(skip_serializing_if = "Option::is_none")]
178+
tmdb: Option<u64>,
179+
}
180+
181+
/// Request body for rating items
182+
#[derive(Debug, Clone, Serialize)]
183+
struct RatingRequest {
184+
#[serde(skip_serializing_if = "Option::is_none")]
185+
movies: Option<Vec<RatingMovie>>,
186+
#[serde(skip_serializing_if = "Option::is_none")]
187+
episodes: Option<Vec<RatingEpisode>>,
188+
}
189+
190+
#[derive(Debug, Clone, Serialize)]
191+
struct RatingMovie {
192+
title: String,
193+
year: u16,
194+
ids: HistoryIds,
195+
rating: u32, // 1-10
196+
}
197+
198+
// TODO: Add support for rating TV episodes
199+
#[derive(Debug, Clone, Serialize)]
200+
struct RatingEpisode {
201+
ids: HistoryIds,
202+
rating: u32, // 1-10
203+
}
204+
151205
// ========== TraktProvider ==========
152206

153207
/// Trakt.tv content provider
@@ -207,6 +261,106 @@ impl TraktProvider {
207261
req
208262
}
209263

264+
// ========== Public API methods ==========
265+
266+
/// Mark a movie as watched in Trakt history
267+
pub async fn mark_movie_watched(
268+
&self,
269+
title: String,
270+
year: u16,
271+
tmdb_id: Option<u64>,
272+
) -> Result<(), ProviderError> {
273+
if self.access_token.is_none() {
274+
return Err(ProviderError::NotAuthenticated);
275+
}
276+
277+
let request = HistoryRequest {
278+
movies: Some(vec![HistoryMovie {
279+
title,
280+
year,
281+
ids: HistoryIds { tmdb: tmdb_id },
282+
}]),
283+
episodes: None,
284+
};
285+
286+
debug!("marking movie as watched: {:?}", request);
287+
288+
let response = self
289+
.request(reqwest::Method::POST, "/sync/history")
290+
.json(&request)
291+
.send()
292+
.await?;
293+
294+
let status = response.status();
295+
if !status.is_success() {
296+
let error_text = response.text().await.unwrap_or_default();
297+
let error_msg = match status.as_u16() {
298+
401 => "Authentication failed - please re-authenticate with Trakt".to_string(),
299+
422 => format!("Validation error - invalid data: {}", error_text),
300+
429 => "Rate limit exceeded - please try again later".to_string(),
301+
500..=599 => "Trakt server error - please try again later".to_string(),
302+
_ => format!("Failed to mark as watched (status {}): {}", status, error_text),
303+
};
304+
return Err(ProviderError::InvalidResponse(error_msg));
305+
}
306+
307+
debug!("Successfully marked movie as watched on Trakt");
308+
Ok(())
309+
}
310+
311+
/// Rate a movie on Trakt (1-10)
312+
pub async fn rate_movie(
313+
&self,
314+
title: String,
315+
year: u16,
316+
tmdb_id: Option<u64>,
317+
rating: u32,
318+
) -> Result<(), ProviderError> {
319+
if self.access_token.is_none() {
320+
return Err(ProviderError::NotAuthenticated);
321+
}
322+
323+
if !(1..=10).contains(&rating) {
324+
return Err(ProviderError::InvalidResponse(
325+
"Rating must be between 1 and 10".to_string(),
326+
));
327+
}
328+
329+
let request = RatingRequest {
330+
movies: Some(vec![RatingMovie {
331+
title,
332+
year,
333+
ids: HistoryIds { tmdb: tmdb_id },
334+
rating,
335+
}]),
336+
episodes: None,
337+
};
338+
339+
debug!("rating movie: {:?}", request);
340+
341+
let response = self
342+
.request(reqwest::Method::POST, "/sync/ratings")
343+
.json(&request)
344+
.send()
345+
.await?;
346+
347+
let status = response.status();
348+
if !status.is_success() {
349+
let error_text = response.text().await.unwrap_or_default();
350+
let error_msg = match status.as_u16() {
351+
401 => "Authentication failed - please re-authenticate with Trakt".to_string(),
352+
422 => format!("Validation error - invalid rating data: {}", error_text),
353+
429 => "Rate limit exceeded - please try again later".to_string(),
354+
500..=599 => "Trakt server error - please try again later".to_string(),
355+
_ => format!("Failed to rate (status {}): {}", status, error_text),
356+
};
357+
return Err(ProviderError::InvalidResponse(error_msg));
358+
}
359+
360+
debug!("Successfully rated movie on Trakt");
361+
Ok(())
362+
}
363+
210364
// ========== Internal API methods ==========
211365

212366
async fn fetch_watchlist(&self) -> Result<Vec<WatchlistItem>, ProviderError> {

src/tui/app.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,13 @@ pub struct App {
289289
pub show_resume_prompt: bool,
290290
pub resume_progress: f64, // Progress percentage to resume from
291291

292+
// Subtitle warning
293+
pub show_subtitle_warning: bool,
294+
295+
// Trakt rating prompt
296+
pub show_rating_prompt: bool,
297+
pub selected_rating: Option<u32>, // None = not selected, Some(1-10) = rating
298+
292299
// Playback tracking (from mpv IPC)
293300
pub playback_progress: f64, // Actual playback progress from player
294301

@@ -420,6 +427,9 @@ impl App {
420427
wizard_edit_buffer: String::new(),
421428
show_resume_prompt: false,
422429
resume_progress: 0.0,
430+
show_subtitle_warning: false,
431+
show_rating_prompt: false,
432+
selected_rating: None,
423433
playback_progress: 0.0,
424434
racing_message: None,
425435
discovery_rows: Vec::new(),
@@ -636,4 +646,9 @@ impl App {
636646
.get(self.selected_row_index)
637647
.and_then(|row| row.items.get(self.selected_item_index))
638648
}
649+
650+
/// Check if any prompts are currently active (resume, subtitle warning, or rating)
651+
pub fn has_active_prompt(&self) -> bool {
652+
self.show_resume_prompt || self.show_subtitle_warning || self.show_rating_prompt
653+
}
639654
}

0 commit comments

Comments
 (0)