CjTUI update
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user