CjTUI update

This commit is contained in:
2026-02-17 11:30:59 -06:00
parent 046605797f
commit 12c69e5838
7 changed files with 34 additions and 19 deletions

2
Cargo.lock generated
View File

@@ -166,7 +166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "cj-jftui" name = "cjtui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"color-eyre", "color-eyre",

View File

@@ -1,5 +1,5 @@
[package] [package]
name = "cj-jftui" name = "cjtui"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
authors = ["Christopher Warren Johnson III"] authors = ["Christopher Warren Johnson III"]

View File

@@ -1,18 +1,33 @@
# CjTUI
*Chris Jellyfin Terminal User Interface*
This is the first actually useful programming project I have completed. This is the first actually useful programming project I have completed.
I still can't write in rust because of generative AI. I still can't write in rust because of generative AI.
Maybe one day when I am better at programming I will refactor the codebase and figure out how to hand code a transcoding option that actually is compatible with Jellyfin API.
## Features
* **Blazingly Fast Speed**
* Online and Offline Playback
* Robust Download Connectivity
* Individual Download and Batch Download Support
* Download Queue Management
* Local Media Management
* Autoplay and Resume functionality
* Playlist and Collection Support
* Persistent Auth
## System Dependencies on ## System Dependencies on
**MPV** and **ARIA2C** **MPV** and **ARIA2C**
## Persistent Storage ## Persistent Storage
All application data is stored in: All application data is stored in:
~/.config/cj-tui/ ~/.config/cjtui/
* config.toml: Server URL and API key is stored. * config.toml: Server URL and API key is stored.
* downloads/: Local download data, used for local playback and offline navigation. * downloads/: Local download data, used for local playback and offline navigation.
* temp/: Temporary download data during ARIA2C functionality. * temp/: Temporary download data during ARIA2C functionality.
## Controls: ## Controls
##### General Interface Controls ##### General Interface Controls
* **Up**/K: Up * **Up**/K: Up
* **Right**/L/**Enter**: Drilldown/Enter * **Right**/L/**Enter**: Drilldown/Enter

View File

@@ -199,11 +199,11 @@ impl DownloadManager {
// ---------- directory helpers ---------- // ---------- directory helpers ----------
const DOWNLOAD_MARKER_EXT: &str = "cj-jftui.json"; const DOWNLOAD_MARKER_EXT: &str = "cjtui.json";
fn marker_path_for_media(media_path: &Path) -> PathBuf { fn marker_path_for_media(media_path: &Path) -> PathBuf {
// Example: // Example:
// Movie.mkv -> Movie.mkv.cj-jftui.json // Movie.mkv -> Movie.mkv.cjtui.json
let file_name = media_path let file_name = media_path
.file_name() .file_name()
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
@@ -247,7 +247,7 @@ fn scan_dir_for_markers(dir: &Path, out: &mut HashSet<String>) -> Result<(), Str
continue; continue;
} }
// Look for *.cj-jftui.json // Look for *.cjtui.json
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or(""); let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
if !name.ends_with(DOWNLOAD_MARKER_EXT) { if !name.ends_with(DOWNLOAD_MARKER_EXT) {
continue; continue;
@@ -1115,7 +1115,7 @@ fn aria2_http_post(body: &str) -> Result<String, String> {
fn aria2_rpc_call(method: &str, params: Value) -> Result<Value, String> { fn aria2_rpc_call(method: &str, params: Value) -> Result<Value, String> {
let payload = json!({ let payload = json!({
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": "cj-jftui", "id": "cjtui",
"method": method, "method": method,
"params": params, "params": params,
}); });
@@ -1178,7 +1178,7 @@ fn aria2_front_status(state: &Arc<Mutex<DownloadState>>) -> Option<String> {
/// If this item_id has a local download marker, return the local media file path. /// If this item_id has a local download marker, return the local media file path.
/// Marker files are written alongside the media file as: /// Marker files are written alongside the media file as:
/// <media_filename>.<DOWNLOAD_MARKER_EXT> /// <media_filename>.<DOWNLOAD_MARKER_EXT>
/// where DOWNLOAD_MARKER_EXT == "cj-jftui.json" /// where DOWNLOAD_MARKER_EXT == "cjtui.json"
pub fn local_media_path_for_item_id(item_id: &str) -> Option<PathBuf> { pub fn local_media_path_for_item_id(item_id: &str) -> Option<PathBuf> {
fn walk(dir: &Path, item_id: &str) -> Option<PathBuf> { fn walk(dir: &Path, item_id: &str) -> Option<PathBuf> {
if !dir.exists() { if !dir.exists() {
@@ -1229,7 +1229,7 @@ pub fn local_media_path_for_item_id(item_id: &str) -> Option<PathBuf> {
} }
// Delete ALL local copies of an item by scanning *.cj-jftui.json markers recursively. // Delete ALL local copies of an item by scanning *.cjtui.json markers recursively.
// Returns how many media files we attempted to remove (count of matched markers). // Returns how many media files we attempted to remove (count of matched markers).
pub fn delete_local_by_item_id(item_id: &str) -> Result<usize, String> { pub fn delete_local_by_item_id(item_id: &str) -> Result<usize, String> {
let root = downloads_root()?; let root = downloads_root()?;
@@ -1314,7 +1314,7 @@ fn delete_in_dir_by_item_id(dir: &Path, item_id: &str, deleted: &mut usize) -> R
Ok(()) Ok(())
} }
// Reads sibling marker "<mediafilename>.cj-jftui.json" and returns item_id if present. // Reads sibling marker "<mediafilename>.cjtui.json" and returns item_id if present.
fn item_id_for_media_path(media_path: &Path) -> Result<Option<String>, String> { fn item_id_for_media_path(media_path: &Path) -> Result<Option<String>, String> {
let fname = match media_path.file_name().and_then(|s| s.to_str()) { let fname = match media_path.file_name().and_then(|s| s.to_str()) {
Some(s) => s, Some(s) => s,

View File

@@ -78,7 +78,7 @@ impl JellyfinClient {
let url = format!("{}/Users/AuthenticateByName", self.base_url); let url = format!("{}/Users/AuthenticateByName", self.base_url);
let auth_header = format!( let auth_header = format!(
"MediaBrowser Client=\"cj-jftui\", Device=\"kitty\", DeviceId=\"cj-jftui\", Version=\"{}\"", "MediaBrowser Client=\"cjtui\", Device=\"kitty\", DeviceId=\"cjtui\", Version=\"{}\"",
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
); );
@@ -206,7 +206,7 @@ impl JellyfinClient {
fn auth_header_value(&self) -> String { fn auth_header_value(&self) -> String {
// Identify as a client and include the token. // Identify as a client and include the token.
format!( format!(
"MediaBrowser Client=\"cj-jftui\", Device=\"kitty\", DeviceId=\"cj-jftui\", Version=\"{}\", Token=\"{}\"", "MediaBrowser Client=\"cjtui\", Device=\"kitty\", DeviceId=\"cjtui\", Version=\"{}\", Token=\"{}\"",
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
self.token self.token
) )

View File

@@ -23,11 +23,11 @@ pub fn cache_dir() -> Option<PathBuf> {
} }
pub fn app_config_dir() -> Option<PathBuf> { pub fn app_config_dir() -> Option<PathBuf> {
config_dir().map(|d| d.join("cj-jftui")) config_dir().map(|d| d.join("cjtui"))
} }
pub fn app_cache_dir() -> Option<PathBuf> { pub fn app_cache_dir() -> Option<PathBuf> {
cache_dir().map(|d| d.join("cj-jftui")) cache_dir().map(|d| d.join("cjtui"))
} }
pub fn posters_cache_dir() -> Option<PathBuf> { pub fn posters_cache_dir() -> Option<PathBuf> {

View File

@@ -1848,7 +1848,7 @@ fn ui(frame: &mut Frame, app: &App) {
// Top // Top
//frame.render_widget( //frame.render_widget(
// Paragraph::new("cj-jftui").block(Block::bordered()), // Paragraph::new("cjtui").block(Block::bordered()),
// panes.top, // panes.top,
//); //);
@@ -1859,7 +1859,7 @@ fn ui(frame: &mut Frame, app: &App) {
let [top_l, top_r] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(40)]) let [top_l, top_r] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(40)])
.areas(top_inner); .areas(top_inner);
frame.render_widget(Paragraph::new("cj-jftui"), top_l); frame.render_widget(Paragraph::new("cjtui"), top_l);
let server = app let server = app
.config .config
.as_ref() .as_ref()
@@ -2170,7 +2170,7 @@ fn open_children(app: &mut App, title: String, query: core::jellyfin::LibraryQue
fn mpv_socket_path() -> PathBuf { fn mpv_socket_path() -> PathBuf {
let mut p = std::env::temp_dir(); let mut p = std::env::temp_dir();
let pid = std::process::id(); let pid = std::process::id();
p.push(format!("cj-jftui-mpv-{}.sock", pid)); p.push(format!("cjtui-mpv-{}.sock", pid));
p p
} }
@@ -3570,7 +3570,7 @@ fn offline_list_dir(dir: &std::path::Path) -> Vec<OfflineEntry> {
fn is_hidden_sidecar(name: &str) -> bool { fn is_hidden_sidecar(name: &str) -> bool {
name.ends_with(".cj-jftui.json") || name.ends_with(".aria2") || name.ends_with(".part") name.ends_with(".cjtui.json") || name.ends_with(".aria2") || name.ends_with(".part")
} }
// Returns true if this directory contains any "visible" file or any subdir that does. // Returns true if this directory contains any "visible" file or any subdir that does.