Fixing Multi-Device Compatibility
This commit is contained in:
+3
-3
@@ -1,3 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
/*.zip
|
/*.zip
|
||||||
*.zip
|
*.zip
|
||||||
|
|||||||
Generated
+2764
-2764
File diff suppressed because it is too large
Load Diff
+12
-12
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cjtui"
|
name = "cjtui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Christopher Warren Johnson III"]
|
authors = ["Christopher Warren Johnson III"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ratatui = "0.30.0"
|
ratatui = "0.30.0"
|
||||||
color-eyre = "0.6.3"
|
color-eyre = "0.6.3"
|
||||||
crossterm = "0.29.0"
|
crossterm = "0.29.0"
|
||||||
serde_json ="1"
|
serde_json ="1"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
@@ -1,123 +1,123 @@
|
|||||||
# CjTUI
|
# CjTUI
|
||||||
*Chris Jellyfin Terminal User Interface*
|
*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.
|
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.
|
||||||
|
|
||||||
Linux Only Curreently.
|
Linux Only Curreently.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* **Blazingly Fast Speed**
|
* **Blazingly Fast Speed**
|
||||||
* Online and Offline Playback
|
* Online and Offline Playback
|
||||||
* Robust Download Connectivity
|
* Robust Download Connectivity
|
||||||
* Individual Download and Batch Download Support
|
* Individual Download and Batch Download Support
|
||||||
* Download Queue Management
|
* Download Queue Management
|
||||||
* Local Media Management
|
* Local Media Management
|
||||||
* Autoplay and Resume functionality
|
* Autoplay and Resume functionality
|
||||||
* Sends Current Watch Position to the Server
|
* Sends Current Watch Position to the Server
|
||||||
* Playlist and Collection Support
|
* Playlist and Collection Support
|
||||||
* Persistent Auth
|
* 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/cjtui/
|
~/.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
|
||||||
* **Down**/J: Down
|
* **Down**/J: Down
|
||||||
* **Left**/H/**Backspace**/Esc: Back
|
* **Left**/H/**Backspace**/Esc: Back
|
||||||
* q: Return to the home screen or quit.
|
* q: Return to the home screen or quit.
|
||||||
* /: Search: Click once to search and clicking / multiple times cycles through search categories.
|
* /: Search: Click once to search and clicking / multiple times cycles through search categories.
|
||||||
|
|
||||||
##### __How to Watch Media__
|
##### __How to Watch Media__
|
||||||
* p: Play: Starts a show playing at the beginning and will use offline playback if available.
|
* p: Play: Starts a show playing at the beginning and will use offline playback if available.
|
||||||
* P: AutoPlay: Starts a show playing at the beginning and will use offline playback if available. Turns on auto play mode which will play the next episode in series or song in the playlist.
|
* P: AutoPlay: Starts a show playing at the beginning and will use offline playback if available. Turns on auto play mode which will play the next episode in series or song in the playlist.
|
||||||
* r: Resume: If there is a pause position online, it will stream beginning at the paused position.
|
* r: Resume: If there is a pause position online, it will stream beginning at the paused position.
|
||||||
* R: AutoResume: Resume + Autoplay flag is on.
|
* R: AutoResume: Resume + Autoplay flag is on.
|
||||||
* w: Watched: Marks as watched.
|
* w: Watched: Marks as watched.
|
||||||
* W: Not Watched: Marks as unwatched.
|
* W: Not Watched: Marks as unwatched.
|
||||||
|
|
||||||
##### __Media Player Control__
|
##### __Media Player Control__
|
||||||
* Space: Pause
|
* Space: Pause
|
||||||
* x: close mpv
|
* x: close mpv
|
||||||
* f: full screen media player
|
* f: full screen media player
|
||||||
* a: cycle audio track
|
* a: cycle audio track
|
||||||
* s: cycle subtitles
|
* s: cycle subtitles
|
||||||
* v: change subtitle visibility.
|
* v: change subtitle visibility.
|
||||||
|
|
||||||
##### __Local File Management__
|
##### __Local File Management__
|
||||||
* d: Queue file for download
|
* d: Queue file for download
|
||||||
* D: Enter the download visual queue.
|
* D: Enter the download visual queue.
|
||||||
* (d on an item in the queue): Remove item from queue.
|
* (d on an item in the queue): Remove item from queue.
|
||||||
* X: Deletes the local file if the file exists.
|
* X: Deletes the local file if the file exists.
|
||||||
* o: Enter offline playback/Attempt to ping the server to join online mode. (Local downloaded database is available during this mode)
|
* o: Enter offline playback/Attempt to ping the server to join online mode. (Local downloaded database is available during this mode)
|
||||||
|
|
||||||
##### __Admin Controls__
|
##### __Admin Controls__
|
||||||
* /\reset"Enter": Deletes the config file and exits the program. Reloading will prompt URL, Username, and Password Input.
|
* /\reset"Enter": Deletes the config file and exits the program. Reloading will prompt URL, Username, and Password Input.
|
||||||
|
|
||||||
## Installation Instructions
|
## Installation Instructions
|
||||||
|
|
||||||
### __Nix/NixOS__
|
### __Nix/NixOS__
|
||||||
#### __Run without installing:__
|
#### __Run without installing:__
|
||||||
```bash
|
```bash
|
||||||
nix run git+https://git.cwj3rcb.com/siege/CjTui
|
nix run git+https://git.cwj3rcb.com/siege/CjTui
|
||||||
```
|
```
|
||||||
|
|
||||||
#### __Build manually:__
|
#### __Build manually:__
|
||||||
```bash
|
```bash
|
||||||
nix build git+https://git.cwj3rcb.com/siege/CjTui
|
nix build git+https://git.cwj3rcb.com/siege/CjTui
|
||||||
./result/bin/cjtui
|
./result/bin/cjtui
|
||||||
```
|
```
|
||||||
|
|
||||||
#### __Install on NixOS with flakes:__
|
#### __Install on NixOS with flakes:__
|
||||||
Add CJTUI to your `flake.nix` inputs
|
Add CJTUI to your `flake.nix` inputs
|
||||||
```nix
|
```nix
|
||||||
inputs.cjtui.url = "git+https://git.cwj3rcb.com/siege/CjTui";
|
inputs.cjtui.url = "git+https://git.cwj3rcb.com/siege/CjTui";
|
||||||
```
|
```
|
||||||
|
|
||||||
Then install the package
|
Then install the package
|
||||||
```nix
|
```nix
|
||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
cjtui.packages.${pkgs.system}.default
|
cjtui.packages.${pkgs.system}.default
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
If your packages are in `configuration.nix`, pass `cjtui` through `specialArgs` from your system flake
|
If your packages are in `configuration.nix`, pass `cjtui` through `specialArgs` from your system flake
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
specialArgs = {
|
specialArgs = {
|
||||||
inherit cjtui;
|
inherit cjtui;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Then your `configuration.nix` can accept it
|
Then your `configuration.nix` can accept it
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{ pkgs, cjtui, ... }:
|
{ pkgs, cjtui, ... }:
|
||||||
```
|
```
|
||||||
|
|
||||||
CJTUI pins its own `nixpkgs` input so it uses tested versions of `mpv` and `aria2c`.
|
CJTUI pins its own `nixpkgs` input so it uses tested versions of `mpv` and `aria2c`.
|
||||||
|
|
||||||
Do **not** add
|
Do **not** add
|
||||||
```nix
|
```nix
|
||||||
inputs.cjtui.inputs.nixpkgs.follows = "nixpkgs";
|
inputs.cjtui.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
```
|
```
|
||||||
|
|
||||||
unless you intentionally want CJTUI to use your system’s versions of `mpv` and `aria2c`.
|
unless you intentionally want CJTUI to use your system’s versions of `mpv` and `aria2c`.
|
||||||
|
|
||||||
### __General Install__
|
### __General Install__
|
||||||
Install mpv and aria2c with your package manager of choice then ->
|
Install mpv and aria2c with your package manager of choice then ->
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo install --git https://git.cwj3rcb.com/siege/CjTui
|
cargo install --git https://git.cwj3rcb.com/siege/CjTui
|
||||||
```
|
```
|
||||||
Generated
+27
-27
@@ -1,27 +1,27 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781577229,
|
"lastModified": 1781577229,
|
||||||
"narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=",
|
"narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "567a49d1913ce81ac6e9582e3553dd90a955875f",
|
"rev": "567a49d1913ce81ac6e9582e3553dd90a955875f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-unstable",
|
"ref": "nixos-unstable",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
"version": 7
|
"version": 7
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
{
|
{
|
||||||
description = "CJTUI - Chris Jellyfin Terminal User Interface";
|
description = "CJTUI - Chris Jellyfin Terminal User Interface";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs }:
|
outputs = { self, nixpkgs }:
|
||||||
let
|
let
|
||||||
systems = [ "x86_64-linux" "aarch64-linux" ];
|
systems = [ "x86_64-linux" "aarch64-linux" ];
|
||||||
|
|
||||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||||
|
|
||||||
pkgsFor = system: import nixpkgs {
|
pkgsFor = system: import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
};
|
};
|
||||||
in {
|
in {
|
||||||
packages = forAllSystems (system:
|
packages = forAllSystems (system:
|
||||||
let
|
let
|
||||||
pkgs = pkgsFor system;
|
pkgs = pkgsFor system;
|
||||||
in {
|
in {
|
||||||
default = pkgs.rustPlatform.buildRustPackage {
|
default = pkgs.rustPlatform.buildRustPackage {
|
||||||
pname = "cjtui";
|
pname = "cjtui";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
||||||
cargoLock = {
|
cargoLock = {
|
||||||
lockFile = ./Cargo.lock;
|
lockFile = ./Cargo.lock;
|
||||||
};
|
};
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pkgs.makeWrapper
|
pkgs.makeWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
wrapProgram $out/bin/cjtui \
|
wrapProgram $out/bin/cjtui \
|
||||||
--prefix PATH : ${pkgs.lib.makeBinPath [
|
--prefix PATH : ${pkgs.lib.makeBinPath [
|
||||||
pkgs.mpv
|
pkgs.mpv
|
||||||
pkgs.aria2
|
pkgs.aria2
|
||||||
]}
|
]}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
apps = forAllSystems (system: {
|
apps = forAllSystems (system: {
|
||||||
default = {
|
default = {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = "${self.packages.${system}.default}/bin/cjtui";
|
program = "${self.packages.${system}.default}/bin/cjtui";
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+156
-105
@@ -1,105 +1,156 @@
|
|||||||
use std::{fs, io, path::PathBuf};
|
use std::{fs, io, path::PathBuf};
|
||||||
|
|
||||||
use super::paths;
|
use super::paths;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
pub access_token: Option<String>,
|
pub access_token: Option<String>,
|
||||||
pub user_id: Option<String>,
|
pub user_id: Option<String>,
|
||||||
}
|
/// Stable, per-installation identifier sent to Jellyfin as DeviceId.
|
||||||
|
/// Must be unique per machine: Jellyfin keys sessions on (DeviceId, Client),
|
||||||
|
/// so two machines sharing one DeviceId invalidate each other's tokens.
|
||||||
#[derive(Debug)]
|
pub device_id: Option<String>,
|
||||||
pub enum ConfigError {
|
}
|
||||||
NotFound(PathBuf),
|
|
||||||
Io(io::Error),
|
|
||||||
Parse(String),
|
#[derive(Debug)]
|
||||||
}
|
pub enum ConfigError {
|
||||||
|
NotFound(PathBuf),
|
||||||
impl From<io::Error> for ConfigError {
|
Io(io::Error),
|
||||||
fn from(e: io::Error) -> Self {
|
Parse(String),
|
||||||
Self::Io(e)
|
}
|
||||||
}
|
|
||||||
}
|
impl From<io::Error> for ConfigError {
|
||||||
|
fn from(e: io::Error) -> Self {
|
||||||
pub fn config_path() -> Option<PathBuf> {
|
Self::Io(e)
|
||||||
paths::app_config_dir().map(|d| d.join("config.toml"))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Very small TOML-ish parser for now (no deps).
|
pub fn config_path() -> Option<PathBuf> {
|
||||||
// Format:
|
paths::app_config_dir().map(|d| d.join("config.toml"))
|
||||||
// base_url = "http://..."
|
}
|
||||||
// access_token = "..."
|
|
||||||
pub fn load_config() -> Result<Config, ConfigError> {
|
// Very small TOML-ish parser for now (no deps).
|
||||||
let path = config_path().ok_or_else(|| ConfigError::Parse("No HOME/XDG dirs".into()))?;
|
// Format:
|
||||||
if !path.exists() {
|
// base_url = "http://..."
|
||||||
return Err(ConfigError::NotFound(path));
|
// access_token = "..."
|
||||||
}
|
pub fn load_config() -> Result<Config, ConfigError> {
|
||||||
|
let path = config_path().ok_or_else(|| ConfigError::Parse("No HOME/XDG dirs".into()))?;
|
||||||
let s = fs::read_to_string(&path)?;
|
if !path.exists() {
|
||||||
parse_config(&s).map_err(ConfigError::Parse)
|
return Err(ConfigError::NotFound(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_config(s: &str) -> Result<Config, String> {
|
let s = fs::read_to_string(&path)?;
|
||||||
let mut base_url: Option<String> = None;
|
parse_config(&s).map_err(ConfigError::Parse)
|
||||||
let mut access_token: Option<String> = None;
|
}
|
||||||
let mut user_id: Option<String> = None;
|
|
||||||
|
fn parse_config(s: &str) -> Result<Config, String> {
|
||||||
for (i, line) in s.lines().enumerate() {
|
let mut base_url: Option<String> = None;
|
||||||
let line = line.trim();
|
let mut access_token: Option<String> = None;
|
||||||
if line.is_empty() || line.starts_with('#') {
|
let mut user_id: Option<String> = None;
|
||||||
continue;
|
let mut device_id: Option<String> = None;
|
||||||
}
|
|
||||||
|
for (i, line) in s.lines().enumerate() {
|
||||||
let (k, v) = line
|
let line = line.trim();
|
||||||
.split_once('=')
|
if line.is_empty() || line.starts_with('#') {
|
||||||
.ok_or_else(|| format!("Line {}: expected key = value", i + 1))?;
|
continue;
|
||||||
|
}
|
||||||
let key = k.trim();
|
|
||||||
let mut val = v.trim().to_string();
|
let (k, v) = line
|
||||||
|
.split_once('=')
|
||||||
// strip quotes if present
|
.ok_or_else(|| format!("Line {}: expected key = value", i + 1))?;
|
||||||
if val.starts_with('"') && val.ends_with('"') && val.len() >= 2 {
|
|
||||||
val = val[1..val.len() - 1].to_string();
|
let key = k.trim();
|
||||||
}
|
let mut val = v.trim().to_string();
|
||||||
|
|
||||||
match key {
|
// strip quotes if present
|
||||||
"base_url" => base_url = Some(val),
|
if val.starts_with('"') && val.ends_with('"') && val.len() >= 2 {
|
||||||
"access_token" => access_token = Some(val),
|
val = val[1..val.len() - 1].to_string();
|
||||||
"user_id" => user_id = Some(val),
|
}
|
||||||
_ => {}
|
|
||||||
}
|
match key {
|
||||||
}
|
"base_url" => base_url = Some(val),
|
||||||
|
"access_token" => access_token = Some(val),
|
||||||
let base_url = base_url.ok_or_else(|| "Missing base_url".to_string())?;
|
"user_id" => user_id = Some(val),
|
||||||
|
"device_id" => device_id = Some(val),
|
||||||
Ok(Config {
|
_ => {}
|
||||||
base_url,
|
}
|
||||||
access_token,
|
}
|
||||||
user_id,
|
|
||||||
})
|
let base_url = base_url.ok_or_else(|| "Missing base_url".to_string())?;
|
||||||
}
|
|
||||||
|
Ok(Config {
|
||||||
pub fn save_config(cfg: &Config) -> Result<(), ConfigError> {
|
base_url,
|
||||||
let dir = paths::app_config_dir()
|
access_token,
|
||||||
.ok_or_else(|| ConfigError::Parse("No HOME/XDG dirs".into()))?;
|
user_id,
|
||||||
std::fs::create_dir_all(&dir)?;
|
device_id,
|
||||||
|
})
|
||||||
let path = dir.join("config.toml");
|
}
|
||||||
|
|
||||||
let mut out = String::new();
|
pub fn save_config(cfg: &Config) -> Result<(), ConfigError> {
|
||||||
|
let dir = paths::app_config_dir()
|
||||||
out.push_str(&format!("base_url = \"{}\"\n", cfg.base_url));
|
.ok_or_else(|| ConfigError::Parse("No HOME/XDG dirs".into()))?;
|
||||||
|
std::fs::create_dir_all(&dir)?;
|
||||||
if let Some(t) = &cfg.access_token {
|
|
||||||
out.push_str(&format!("access_token = \"{}\"\n", t));
|
let path = dir.join("config.toml");
|
||||||
}
|
|
||||||
|
let mut out = String::new();
|
||||||
if let Some(u) = &cfg.user_id {
|
|
||||||
out.push_str(&format!("user_id = \"{}\"\n", u));
|
out.push_str(&format!("base_url = \"{}\"\n", cfg.base_url));
|
||||||
}
|
|
||||||
|
if let Some(t) = &cfg.access_token {
|
||||||
std::fs::write(path, out)?;
|
out.push_str(&format!("access_token = \"{}\"\n", t));
|
||||||
Ok(())
|
}
|
||||||
}
|
|
||||||
|
if let Some(u) = &cfg.user_id {
|
||||||
|
out.push_str(&format!("user_id = \"{}\"\n", u));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(d) = &cfg.device_id {
|
||||||
|
out.push_str(&format!("device_id = \"{}\"\n", d));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::write(path, out)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns this installation's DeviceId, generating and persisting one on first use.
|
||||||
|
///
|
||||||
|
/// Jellyfin treats a DeviceId as a physical device. Logging in from a second
|
||||||
|
/// machine with the same DeviceId makes the server rebind that device's session
|
||||||
|
/// and revoke the first machine's access token, so this value must never be
|
||||||
|
/// shared between installs (and must survive restarts, or every launch would
|
||||||
|
/// register a fresh device on the server).
|
||||||
|
pub fn ensure_device_id(cfg: &mut Config) -> String {
|
||||||
|
if let Some(d) = cfg.device_id.as_deref() {
|
||||||
|
if !d.trim().is_empty() {
|
||||||
|
return d.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = generate_device_id();
|
||||||
|
cfg.device_id = Some(id.clone());
|
||||||
|
// Best effort: if the write fails we still use the id for this run, and try
|
||||||
|
// again next launch rather than failing the login outright.
|
||||||
|
let _ = save_config(cfg);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_device_id() -> String {
|
||||||
|
// The kernel hands us a real random UUID, no crate needed.
|
||||||
|
if let Ok(s) = fs::read_to_string("/proc/sys/kernel/random/uuid") {
|
||||||
|
let s = s.trim();
|
||||||
|
if !s.is_empty() {
|
||||||
|
return s.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for anything without procfs: mix clock and pid.
|
||||||
|
let nanos = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
format!("cjtui-{:x}-{:x}", nanos, std::process::id())
|
||||||
|
}
|
||||||
|
|||||||
+1364
-1364
File diff suppressed because it is too large
Load Diff
+695
-674
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod jellyfin;
|
pub mod jellyfin;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
pub mod downloads;
|
pub mod downloads;
|
||||||
+44
-44
@@ -1,44 +1,44 @@
|
|||||||
use std::{env, path::PathBuf};
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
pub fn home_dir() -> Option<PathBuf> {
|
pub fn home_dir() -> Option<PathBuf> {
|
||||||
env::var_os("HOME").map(PathBuf::from)
|
env::var_os("HOME").map(PathBuf::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn config_dir() -> Option<PathBuf> {
|
pub fn config_dir() -> Option<PathBuf> {
|
||||||
// XDG first, fallback to ~/.config
|
// XDG first, fallback to ~/.config
|
||||||
if let Some(p) = env::var_os("XDG_CONFIG_HOME").map(PathBuf::from) {
|
if let Some(p) = env::var_os("XDG_CONFIG_HOME").map(PathBuf::from) {
|
||||||
Some(p)
|
Some(p)
|
||||||
} else {
|
} else {
|
||||||
home_dir().map(|h| h.join(".config"))
|
home_dir().map(|h| h.join(".config"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cache_dir() -> Option<PathBuf> {
|
pub fn cache_dir() -> Option<PathBuf> {
|
||||||
// XDG first, fallback to ~/.cache
|
// XDG first, fallback to ~/.cache
|
||||||
if let Some(p) = env::var_os("XDG_CACHE_HOME").map(PathBuf::from) {
|
if let Some(p) = env::var_os("XDG_CACHE_HOME").map(PathBuf::from) {
|
||||||
Some(p)
|
Some(p)
|
||||||
} else {
|
} else {
|
||||||
home_dir().map(|h| h.join(".cache"))
|
home_dir().map(|h| h.join(".cache"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn app_config_dir() -> Option<PathBuf> {
|
pub fn app_config_dir() -> Option<PathBuf> {
|
||||||
config_dir().map(|d| d.join("cjtui"))
|
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("cjtui"))
|
cache_dir().map(|d| d.join("cjtui"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn posters_cache_dir() -> Option<PathBuf> {
|
pub fn posters_cache_dir() -> Option<PathBuf> {
|
||||||
app_cache_dir().map(|d| d.join("posters"))
|
app_cache_dir().map(|d| d.join("posters"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn downloads_dir() -> Option<PathBuf> {
|
pub fn downloads_dir() -> Option<PathBuf> {
|
||||||
app_config_dir().map(|d| d.join("downloads"))
|
app_config_dir().map(|d| d.join("downloads"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn temp_downloads_dir() -> Option<PathBuf> {
|
pub fn temp_downloads_dir() -> Option<PathBuf> {
|
||||||
app_config_dir().map(|d| d.join("temp"))
|
app_config_dir().map(|d| d.join("temp"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3981
-3973
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user