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"
[[package]]
name = "cj-jftui"
name = "cjtui"
version = "0.1.0"
dependencies = [
"color-eyre",

View File

@@ -1,5 +1,5 @@
[package]
name = "cj-jftui"
name = "cjtui"
version = "0.1.0"
edition = "2024"
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.
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
**MPV** and **ARIA2C**
## Persistent Storage
All application data is stored in:
~/.config/cj-tui/
~/.config/cjtui/
* config.toml: Server URL and API key is stored.
* downloads/: Local download data, used for local playback and offline navigation.
* temp/: Temporary download data during ARIA2C functionality.
## Controls:
## Controls
##### General Interface Controls
* **Up**/K: Up
* **Right**/L/**Enter**: Drilldown/Enter

View File

@@ -199,11 +199,11 @@ impl DownloadManager {
// ---------- 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 {
// Example:
// Movie.mkv -> Movie.mkv.cj-jftui.json
// Movie.mkv -> Movie.mkv.cjtui.json
let file_name = media_path
.file_name()
.and_then(|s| s.to_str())
@@ -247,7 +247,7 @@ fn scan_dir_for_markers(dir: &Path, out: &mut HashSet<String>) -> Result<(), Str
continue;
}
// Look for *.cj-jftui.json
// Look for *.cjtui.json
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
if !name.ends_with(DOWNLOAD_MARKER_EXT) {
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> {
let payload = json!({
"jsonrpc": "2.0",
"id": "cj-jftui",
"id": "cjtui",
"method": method,
"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.
/// Marker files are written alongside the media file as:
/// <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> {
fn walk(dir: &Path, item_id: &str) -> Option<PathBuf> {
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).
pub fn delete_local_by_item_id(item_id: &str) -> Result<usize, String> {
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(())
}
// 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> {
let fname = match media_path.file_name().and_then(|s| s.to_str()) {
Some(s) => s,

View File

@@ -78,7 +78,7 @@ impl JellyfinClient {
let url = format!("{}/Users/AuthenticateByName", self.base_url);
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"),
);
@@ -206,7 +206,7 @@ impl JellyfinClient {
fn auth_header_value(&self) -> String {
// Identify as a client and include the token.
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"),
self.token
)

View File

@@ -23,11 +23,11 @@ pub fn cache_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> {
cache_dir().map(|d| d.join("cj-jftui"))
cache_dir().map(|d| d.join("cjtui"))
}
pub fn posters_cache_dir() -> Option<PathBuf> {

View File

@@ -1848,7 +1848,7 @@ fn ui(frame: &mut Frame, app: &App) {
// Top
//frame.render_widget(
// Paragraph::new("cj-jftui").block(Block::bordered()),
// Paragraph::new("cjtui").block(Block::bordered()),
// 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)])
.areas(top_inner);
frame.render_widget(Paragraph::new("cj-jftui"), top_l);
frame.render_widget(Paragraph::new("cjtui"), top_l);
let server = app
.config
.as_ref()
@@ -2170,7 +2170,7 @@ fn open_children(app: &mut App, title: String, query: core::jellyfin::LibraryQue
fn mpv_socket_path() -> PathBuf {
let mut p = std::env::temp_dir();
let pid = std::process::id();
p.push(format!("cj-jftui-mpv-{}.sock", pid));
p.push(format!("cjtui-mpv-{}.sock", pid));
p
}
@@ -3570,7 +3570,7 @@ fn offline_list_dir(dir: &std::path::Path) -> Vec<OfflineEntry> {
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.