diff --git a/flake.lock b/flake.lock index 04ded3e..a642ed0 100644 --- a/flake.lock +++ b/flake.lock @@ -47,15 +47,16 @@ "rust-analyzer-src": [] }, "locked": { - "lastModified": 1686896497, - "narHash": "sha256-IphIS1KpiFXp/j0v7mEMhtw51uvU5F0mqF2j4//7VAA=", + "lastModified": 1688484237, + "narHash": "sha256-qFUn2taHGe203wm7Oio4UGFz1sAiq+kitRexY3sQ1CA=", "owner": "nix-community", "repo": "fenix", - "rev": "9c69d11badcd78710d7d8665bc3d2e1adc450ffe", + "rev": "626a9e0a84010728b335f14d3982e11b99af7dc6", "type": "github" }, "original": { "owner": "nix-community", + "ref": "monthly", "repo": "fenix", "type": "github" } diff --git a/flake.nix b/flake.nix index c2f7004..38278d4 100644 --- a/flake.nix +++ b/flake.nix @@ -10,7 +10,7 @@ }; fenix = { - url = "github:nix-community/fenix"; + url = "github:nix-community/fenix/monthly"; inputs.nixpkgs.follows = "nixpkgs"; inputs.rust-analyzer-src.follows = ""; }; @@ -21,16 +21,25 @@ url = "github:rustsec/advisory-db"; flake = false; }; + }; outputs = { self, nixpkgs, crane, fenix, flake-utils, advisory-db, ... }: flake-utils.lib.eachDefaultSystem (system: + let + toolchain = (fenix.packages.${system}.toolchainOf { + channel = "nightly"; + date = "2023-07-27"; + sha256 = "1bUA3mqH455LncZMMH1oEBFLWu5TOluJeDZ8iwAsBGs="; + }); + in let pkgs = import nixpkgs { inherit system; }; inherit (pkgs) lib; + inherit toolchain; craneLib = crane.lib.${system}; src = craneLib.cleanCargoSource (craneLib.path ./.); @@ -52,8 +61,7 @@ # MY_CUSTOM_VAR = "some value"; }; - craneLibLLvmTools = craneLib.overrideToolchain - (fenix.packages.${system}.complete.withComponents [ + craneLibLLvmTools = craneLib.overrideToolchain (toolchain.withComponents [ "cargo" "llvm-tools" "rustc" @@ -77,6 +85,8 @@ pkgs.llvmPackages_latest.libclang pkgs.rustPlatform.bindgenHook pkgs.openssl + pkgs.samba + pkgs.samba.dev ]; }); in @@ -145,8 +155,10 @@ # Extra inputs can be added here nativeBuildInputs = with pkgs; [ - cargo - rustc + (toolchain.withComponents [ + "cargo" + "rustc" + ]) pkg-config ]; @@ -158,6 +170,8 @@ pkgs.llvmPackages_latest.bintools pkgs.clang_16 pkgs.openssl + pkgs.samba + pkgs.samba.dev ]; LIBCLANG_PATH = "${pkgs.llvmPackages_latest.libclang.lib}/lib"; @@ -167,6 +181,7 @@ # add dev libraries here (e.g. pkgs.libvmi.dev) pkgs.glibc.dev pkgs.ffmpeg.dev + pkgs.samba.dev ]) # Includes with special directory paths ++ [ diff --git a/src/lib.rs b/src/lib.rs index 6434b1e..75094f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,7 +113,7 @@ pub async fn try_open_nfo(lfs: &mut MultiFs, mut path: PathBuf) -> Result Result { +pub async fn get_metadata(lfs: &mut MultiFs, base_url: Url, path: PathBuf) -> Result { use metadata::media_file::MediaFileMetadata; use metadata::stream::StreamMetadata; @@ -165,7 +165,7 @@ async fn get_metadata(lfs: &mut MultiFs, base_url: Url, path: PathBuf) -> Result }) } -async fn transform_as_nfo(client: &TmdbClient, tmdb_id: u64, lang: Option) -> Result { +pub async fn transform_as_nfo(client: &TmdbClient, tmdb_id: u64, lang: Option) -> Result { let mdr = MovieDetails::new(tmdb_id).with_language(lang.clone()); let md = mdr.execute(&client).await .map_err(|err| anyhow!("Failed to get movie details (id: {}), causes:\n{:?}", tmdb_id, err))?; diff --git a/src/main.rs b/src/main.rs index 9b16757..2592294 100644 --- a/src/main.rs +++ b/src/main.rs @@ -157,23 +157,37 @@ where B: tui::backend::Backend ..Default::default() } }); - state.register_event(AppEvent::MovieManagerEvent(MovieManagerEvent::MovieDiscovered((movie, path)))); + state.register_event(AppEvent::MovieManagerEvent(MovieManagerEvent::MovieDiscovered((movie, i, path)))); } } } } }, AppMessage::MovieManagerMessage(MovieManagerMessage::SearchTitle(title)) => { + use tmdb_api::movie::search::MovieSearch; + use tmdb_api::prelude::Command; let ms = MovieSearch::new(title); - if let Some(results) = ms.execute(&tmdb_client).await { + if let Ok(results) = ms.execute(&tmdb_client).await { state.register_event(AppEvent::MovieManagerEvent(MovieManagerEvent::SearchResults(results.results))); } else { // TODO } }, - AppMessage::MovieManagerMessage(MovieManagerMessage::SaveNfo((nfo, path))) => { - // TODO - unimplemented!() + AppMessage::MovieManagerMessage(MovieManagerMessage::SaveNfo((id, fs_id, mut path))) => { + use std::io::Cursor; + use std::io::Seek; + let mut movie_nfo = mkube::transform_as_nfo(&tmdb_client, id, Some("fr".to_owned())).await?; + let mt = mkube::get_metadata(&mut state.conns[fs_id], (&state.libraries[fs_id]).try_into().expect("Cannot get a baseURL from library."), path.clone()).await?; + movie_nfo.fileinfo = Some(mt); + let nfo_string = quick_xml::se::to_string(&movie_nfo).expect("Failed to produce a valid nfo file."); + + path.set_extension("nfo"); + let mut buf = Cursor::new(Vec::new()); + buf.write_all(br#""#).await?; + buf.write_all(nfo_string.as_bytes()).await?; + let _ = buf.rewind(); + let _ = state.conns[fs_id].as_mut_rfs().create_file(&path, &Metadata::default(), Box::new(buf)) + .map_err(|err| anyhow!("Can't open the nfo file., causes:\n{:?}", err))?; }, AppMessage::Close => { break; diff --git a/src/views/movie_manager/mod.rs b/src/views/movie_manager/mod.rs index 0a32b6e..450c6e9 100644 --- a/src/views/movie_manager/mod.rs +++ b/src/views/movie_manager/mod.rs @@ -6,6 +6,8 @@ use tui::{ }; use std::path::PathBuf; +use crossterm::event::KeyCode; + pub mod details; pub mod table; @@ -14,6 +16,7 @@ pub mod search; use table::{MovieTable, MovieTableState}; use search::{MovieSearch, MovieSearchState}; use crate::{AppEvent, AppState, AppMessage}; +use crate::views::widgets::{Input, InputState}; #[derive(Clone, Debug, Default)] pub struct MovieManager { @@ -35,16 +38,16 @@ impl Default for MovieManagerState { #[derive(Clone, Debug, PartialEq)] pub enum MovieManagerEvent { ClearMovieList, - MovieDiscovered((crate::nfo::Movie, PathBuf)), - MovieUpdated((crate::nfo::Movie, PathBuf)), - SearchMovie((crate::nfo::Movie, PathBuf)), + MovieDiscovered((crate::nfo::Movie, usize, PathBuf)), + MovieUpdated((crate::nfo::Movie, usize, PathBuf)), + SearchMovie((crate::nfo::Movie, usize, PathBuf)), SearchResults(Vec), } #[derive(Clone, Debug, PartialEq)] pub enum MovieManagerMessage { RefreshMovies, SearchTitle(String), - SaveNfo((crate::nfo::Movie, PathBuf)), + SaveNfo((u64, usize, PathBuf)), // tmdb_id, movie_path } impl StatefulWidget for MovieManager { @@ -76,19 +79,33 @@ impl MovieManagerState { match self { MovieManagerState::Table(ref mut state) => { match app_event { - AppEvent::MovieManagerEvent(MovieManagerEvent::SearchMovie((movie, path))) => { + AppEvent::MovieManagerEvent(MovieManagerEvent::SearchMovie((movie, fs_id, path))) => { let mut query_state = InputState::default(); query_state.set_value(&movie.title); let new_state = MovieSearchState { movie_path: path, + movie_fs_id: fs_id, query_state, ..Default::default() }; *self = MovieManagerState::Search(new_state); + true } _ => state.input(app_event), } }, + MovieManagerState::Search(ref mut state) => { + if let AppEvent::KeyEvent(kev) = app_event { + if kev.code == KeyCode::Esc { + *self = Default::default(); + true + } else { + state.input(app_event) + } + } else { + state.input(app_event) + } + }, _ => { false }, } } diff --git a/src/views/movie_manager/search.rs b/src/views/movie_manager/search.rs index 146e2fd..a02dbf4 100644 --- a/src/views/movie_manager/search.rs +++ b/src/views/movie_manager/search.rs @@ -34,13 +34,14 @@ impl Default for MovieSearch { #[derive(Clone, Debug, Default)] pub struct MovieSearchState { - table_state: TableState, - results: Vec, - is_loading: bool, - query_state: InputState, - send_state: ButtonState, - selected: usize, - movie_path: PathBuf, + pub table_state: TableState, + pub results: Vec, + pub is_loading: bool, + pub query_state: InputState, + pub send_state: ButtonState, + pub selected: usize, + pub movie_path: PathBuf, + pub movie_fs_id: usize, } impl StatefulWidget for MovieSearch { @@ -54,9 +55,9 @@ impl StatefulWidget for MovieSearch { Constraint::Percentage(100), ]).split(area); let search_bar = Layout::default() - .direction(Direction::Vertical) + .direction(Direction::Horizontal) .constraints(vec![ - Constraint::Percentage(100), + Constraint::Length(chunks[0].width - 10), Constraint::Min(2), Constraint::Min(8), ]).split(chunks[0]); @@ -69,7 +70,7 @@ impl StatefulWidget for MovieSearch { state.query_state.set_focus(state.selected == 0); state.send_state.focus(state.selected == 1); StatefulWidget::render(self.query, search_bar[0], buf, &mut state.query_state); - StatefulWidget::render(self.send, search_bar[1], buf, &mut state.send_state); + StatefulWidget::render(self.send, search_bar[2], buf, &mut state.send_state); search_block.render(chunks[1], buf); if state.is_loading { Paragraph::new("Searching...").render(inner, buf); @@ -92,7 +93,7 @@ impl StatefulWidget for MovieSearch { ) .widths(&[Constraint::Length(50), Constraint::Length(4), Constraint::Percentage(100)]) .column_spacing(1) - .highlight_style(Style::default().bg(Color::LightRed)); + .highlight_style(Style::default().bg(if state.selected == 2 { Color::LightRed } else { Color::Gray })); StatefulWidget::render(table, inner, buf, &mut state.table_state); } } @@ -106,19 +107,22 @@ impl MovieSearchState { if self.selected == 0 || self.selected == 1 { let sender = MESSAGE_SENDER.get().unwrap(); sender.send(AppMessage::MovieManagerMessage(MovieManagerMessage::SearchTitle(self.query_state.get_value().to_owned()))).unwrap(); + self.is_loading = true; true } else if self.selected == 2 { - /*let sender = MESSAGE_SENDER.get().unwrap(); - sender.send(AppMessage::MovieManagerMessage(MovieManagerMessage::SaveNfo((nfo, self.movie_path.clone())))).unwrap(); - */ - true + if let Some(index) = self.table_state.selected() { + let sender = MESSAGE_SENDER.get().unwrap(); + sender.send(AppMessage::MovieManagerMessage(MovieManagerMessage::SaveNfo((self.results[index].inner.id, self.movie_fs_id, self.movie_path.clone())))).unwrap(); + return true; + } + false } else { false } - } else if kev.code == KeyCode::Up && self.results.len() > 0 { + } else if self.selected == 2 && kev.code == KeyCode::Up && self.results.len() > 0 { self.table_state.select(self.table_state.selected().map(|c| (c + self.results.len() - 1) % self.results.len())); true - } else if kev.code == KeyCode::Down && self.results.len() > 0 { + } else if self.selected == 2 && kev.code == KeyCode::Down && self.results.len() > 0 { self.table_state.select(self.table_state.selected().map(|c| (c + 1) % self.results.len()).or(Some(0))); true } else if kev.code == KeyCode::Tab { @@ -128,9 +132,21 @@ impl MovieSearchState { self.selected = (self.selected + 2) % 3; true } else { - false + if self.selected == 0 { + self.query_state.input(kev) + } else if self.selected == 1 { + self.send_state.input(kev) + } else { + false + } } }, + AppEvent::MovieManagerEvent(MovieManagerEvent::SearchResults(results)) => { + self.results = results; + self.table_state.select(None); + self.is_loading = false; + true + }, _ => { false }, } } diff --git a/src/views/movie_manager/table.rs b/src/views/movie_manager/table.rs index 03d1a4b..fedc699 100644 --- a/src/views/movie_manager/table.rs +++ b/src/views/movie_manager/table.rs @@ -17,7 +17,7 @@ pub struct MovieTable {} #[derive(Clone, Debug, Default)] pub struct MovieTableState { table_state: TableState, - movies: Vec<(Movie, PathBuf)>, + movies: Vec<(Movie, usize, PathBuf)>, is_loading: bool, } @@ -35,7 +35,7 @@ impl StatefulWidget for MovieTable { } let rows : Vec<_> = state.movies.iter() - .map(|(m, _)| { + .map(|(m, _, _)| { let title = m.title.clone(); let year = m.premiered.as_deref().unwrap_or("".into()); let source = m.source.as_deref().unwrap_or("".into()); @@ -63,7 +63,7 @@ impl MovieTableState { pub fn input(&mut self, app_event: AppEvent) -> bool { match app_event { AppEvent::KeyEvent(kev) => { - if kev.code == KeyCode::Char('R') && kev.modifiers == (KeyModifiers::SHIFT | KeyModifiers::CONTROL) && (!self.is_loading) { + if kev.code == KeyCode::Char('r') && (!self.is_loading) { let sender = MESSAGE_SENDER.get().unwrap(); sender.send(AppMessage::MovieManagerMessage(MovieManagerMessage::RefreshMovies)).unwrap(); self.is_loading = true; @@ -96,12 +96,12 @@ impl MovieTableState { self.movies.push(movie); true }, - AppEvent::MovieManagerEvent(MovieManagerEvent::MovieUpdated((movie, path))) => { + AppEvent::MovieManagerEvent(MovieManagerEvent::MovieUpdated((movie, fs_id, path))) => { self.is_loading = false; - if let Some((ind, _)) = self.movies.iter().enumerate().filter(|(_, (_,p))| p == &path).next() { - self.movies[ind] = (movie, path); + if let Some((ind, _)) = self.movies.iter().enumerate().filter(|(_, (_,fi,p))| p == &path && fi == &fs_id).next() { + self.movies[ind] = (movie, fs_id, path); } else { - self.movies.push((movie, path)); + self.movies.push((movie, fs_id, path)); } true },