Fixing Multi-Device Compatibility
This commit is contained in:
+3
-3
@@ -1,3 +1,3 @@
|
||||
/target
|
||||
/*.zip
|
||||
*.zip
|
||||
/target
|
||||
/*.zip
|
||||
*.zip
|
||||
|
||||
Generated
+2764
-2764
File diff suppressed because it is too large
Load Diff
+12
-12
@@ -1,13 +1,13 @@
|
||||
[package]
|
||||
name = "cjtui"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["Christopher Warren Johnson III"]
|
||||
|
||||
[dependencies]
|
||||
ratatui = "0.30.0"
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = "0.29.0"
|
||||
serde_json ="1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
[package]
|
||||
name = "cjtui"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["Christopher Warren Johnson III"]
|
||||
|
||||
[dependencies]
|
||||
ratatui = "0.30.0"
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = "0.29.0"
|
||||
serde_json ="1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
@@ -1,123 +1,123 @@
|
||||
# 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.
|
||||
|
||||
Linux Only Curreently.
|
||||
|
||||
## 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
|
||||
* Sends Current Watch Position to the Server
|
||||
* Playlist and Collection Support
|
||||
* Persistent Auth
|
||||
|
||||
## System Dependencies on
|
||||
**MPV** and **ARIA2C**
|
||||
|
||||
## Persistent Storage
|
||||
All application data is stored in:
|
||||
~/.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
|
||||
##### __General Interface Controls__
|
||||
* **Up**/K: Up
|
||||
* **Right**/L/**Enter**: Drilldown/Enter
|
||||
* **Down**/J: Down
|
||||
* **Left**/H/**Backspace**/Esc: Back
|
||||
* q: Return to the home screen or quit.
|
||||
* /: Search: Click once to search and clicking / multiple times cycles through search categories.
|
||||
|
||||
##### __How to Watch Media__
|
||||
* 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.
|
||||
* r: Resume: If there is a pause position online, it will stream beginning at the paused position.
|
||||
* R: AutoResume: Resume + Autoplay flag is on.
|
||||
* w: Watched: Marks as watched.
|
||||
* W: Not Watched: Marks as unwatched.
|
||||
|
||||
##### __Media Player Control__
|
||||
* Space: Pause
|
||||
* x: close mpv
|
||||
* f: full screen media player
|
||||
* a: cycle audio track
|
||||
* s: cycle subtitles
|
||||
* v: change subtitle visibility.
|
||||
|
||||
##### __Local File Management__
|
||||
* d: Queue file for download
|
||||
* D: Enter the download visual queue.
|
||||
* (d on an item in the queue): Remove item from queue.
|
||||
* 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)
|
||||
|
||||
##### __Admin Controls__
|
||||
* /\reset"Enter": Deletes the config file and exits the program. Reloading will prompt URL, Username, and Password Input.
|
||||
|
||||
## Installation Instructions
|
||||
|
||||
### __Nix/NixOS__
|
||||
#### __Run without installing:__
|
||||
```bash
|
||||
nix run git+https://git.cwj3rcb.com/siege/CjTui
|
||||
```
|
||||
|
||||
#### __Build manually:__
|
||||
```bash
|
||||
nix build git+https://git.cwj3rcb.com/siege/CjTui
|
||||
./result/bin/cjtui
|
||||
```
|
||||
|
||||
#### __Install on NixOS with flakes:__
|
||||
Add CJTUI to your `flake.nix` inputs
|
||||
```nix
|
||||
inputs.cjtui.url = "git+https://git.cwj3rcb.com/siege/CjTui";
|
||||
```
|
||||
|
||||
Then install the package
|
||||
```nix
|
||||
environment.systemPackages = [
|
||||
cjtui.packages.${pkgs.system}.default
|
||||
];
|
||||
```
|
||||
|
||||
If your packages are in `configuration.nix`, pass `cjtui` through `specialArgs` from your system flake
|
||||
|
||||
```nix
|
||||
specialArgs = {
|
||||
inherit cjtui;
|
||||
};
|
||||
```
|
||||
|
||||
Then your `configuration.nix` can accept it
|
||||
|
||||
```nix
|
||||
{ pkgs, cjtui, ... }:
|
||||
```
|
||||
|
||||
CJTUI pins its own `nixpkgs` input so it uses tested versions of `mpv` and `aria2c`.
|
||||
|
||||
Do **not** add
|
||||
```nix
|
||||
inputs.cjtui.inputs.nixpkgs.follows = "nixpkgs";
|
||||
```
|
||||
|
||||
unless you intentionally want CJTUI to use your system’s versions of `mpv` and `aria2c`.
|
||||
|
||||
### __General Install__
|
||||
Install mpv and aria2c with your package manager of choice then ->
|
||||
|
||||
```bash
|
||||
cargo install --git https://git.cwj3rcb.com/siege/CjTui
|
||||
# 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.
|
||||
|
||||
Linux Only Curreently.
|
||||
|
||||
## 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
|
||||
* Sends Current Watch Position to the Server
|
||||
* Playlist and Collection Support
|
||||
* Persistent Auth
|
||||
|
||||
## System Dependencies on
|
||||
**MPV** and **ARIA2C**
|
||||
|
||||
## Persistent Storage
|
||||
All application data is stored in:
|
||||
~/.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
|
||||
##### __General Interface Controls__
|
||||
* **Up**/K: Up
|
||||
* **Right**/L/**Enter**: Drilldown/Enter
|
||||
* **Down**/J: Down
|
||||
* **Left**/H/**Backspace**/Esc: Back
|
||||
* q: Return to the home screen or quit.
|
||||
* /: Search: Click once to search and clicking / multiple times cycles through search categories.
|
||||
|
||||
##### __How to Watch Media__
|
||||
* 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.
|
||||
* r: Resume: If there is a pause position online, it will stream beginning at the paused position.
|
||||
* R: AutoResume: Resume + Autoplay flag is on.
|
||||
* w: Watched: Marks as watched.
|
||||
* W: Not Watched: Marks as unwatched.
|
||||
|
||||
##### __Media Player Control__
|
||||
* Space: Pause
|
||||
* x: close mpv
|
||||
* f: full screen media player
|
||||
* a: cycle audio track
|
||||
* s: cycle subtitles
|
||||
* v: change subtitle visibility.
|
||||
|
||||
##### __Local File Management__
|
||||
* d: Queue file for download
|
||||
* D: Enter the download visual queue.
|
||||
* (d on an item in the queue): Remove item from queue.
|
||||
* 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)
|
||||
|
||||
##### __Admin Controls__
|
||||
* /\reset"Enter": Deletes the config file and exits the program. Reloading will prompt URL, Username, and Password Input.
|
||||
|
||||
## Installation Instructions
|
||||
|
||||
### __Nix/NixOS__
|
||||
#### __Run without installing:__
|
||||
```bash
|
||||
nix run git+https://git.cwj3rcb.com/siege/CjTui
|
||||
```
|
||||
|
||||
#### __Build manually:__
|
||||
```bash
|
||||
nix build git+https://git.cwj3rcb.com/siege/CjTui
|
||||
./result/bin/cjtui
|
||||
```
|
||||
|
||||
#### __Install on NixOS with flakes:__
|
||||
Add CJTUI to your `flake.nix` inputs
|
||||
```nix
|
||||
inputs.cjtui.url = "git+https://git.cwj3rcb.com/siege/CjTui";
|
||||
```
|
||||
|
||||
Then install the package
|
||||
```nix
|
||||
environment.systemPackages = [
|
||||
cjtui.packages.${pkgs.system}.default
|
||||
];
|
||||
```
|
||||
|
||||
If your packages are in `configuration.nix`, pass `cjtui` through `specialArgs` from your system flake
|
||||
|
||||
```nix
|
||||
specialArgs = {
|
||||
inherit cjtui;
|
||||
};
|
||||
```
|
||||
|
||||
Then your `configuration.nix` can accept it
|
||||
|
||||
```nix
|
||||
{ pkgs, cjtui, ... }:
|
||||
```
|
||||
|
||||
CJTUI pins its own `nixpkgs` input so it uses tested versions of `mpv` and `aria2c`.
|
||||
|
||||
Do **not** add
|
||||
```nix
|
||||
inputs.cjtui.inputs.nixpkgs.follows = "nixpkgs";
|
||||
```
|
||||
|
||||
unless you intentionally want CJTUI to use your system’s versions of `mpv` and `aria2c`.
|
||||
|
||||
### __General Install__
|
||||
Install mpv and aria2c with your package manager of choice then ->
|
||||
|
||||
```bash
|
||||
cargo install --git https://git.cwj3rcb.com/siege/CjTui
|
||||
```
|
||||
Generated
+27
-27
@@ -1,27 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1781577229,
|
||||
"narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "567a49d1913ce81ac6e9582e3553dd90a955875f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1781577229,
|
||||
"narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "567a49d1913ce81ac6e9582e3553dd90a955875f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
{
|
||||
description = "CJTUI - Chris Jellyfin Terminal User Interface";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
systems = [ "x86_64-linux" "aarch64-linux" ];
|
||||
|
||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||
|
||||
pkgsFor = system: import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in {
|
||||
packages = forAllSystems (system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in {
|
||||
default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "cjtui";
|
||||
version = "0.1.0";
|
||||
|
||||
src = ./.;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.makeWrapper
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
wrapProgram $out/bin/cjtui \
|
||||
--prefix PATH : ${pkgs.lib.makeBinPath [
|
||||
pkgs.mpv
|
||||
pkgs.aria2
|
||||
]}
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
||||
apps = forAllSystems (system: {
|
||||
default = {
|
||||
type = "app";
|
||||
program = "${self.packages.${system}.default}/bin/cjtui";
|
||||
};
|
||||
});
|
||||
};
|
||||
{
|
||||
description = "CJTUI - Chris Jellyfin Terminal User Interface";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
systems = [ "x86_64-linux" "aarch64-linux" ];
|
||||
|
||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||
|
||||
pkgsFor = system: import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
in {
|
||||
packages = forAllSystems (system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
in {
|
||||
default = pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "cjtui";
|
||||
version = "0.1.0";
|
||||
|
||||
src = ./.;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.makeWrapper
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
wrapProgram $out/bin/cjtui \
|
||||
--prefix PATH : ${pkgs.lib.makeBinPath [
|
||||
pkgs.mpv
|
||||
pkgs.aria2
|
||||
]}
|
||||
'';
|
||||
};
|
||||
});
|
||||
|
||||
apps = forAllSystems (system: {
|
||||
default = {
|
||||
type = "app";
|
||||
program = "${self.packages.${system}.default}/bin/cjtui";
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
+156
-105
@@ -1,105 +1,156 @@
|
||||
use std::{fs, io, path::PathBuf};
|
||||
|
||||
use super::paths;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub base_url: String,
|
||||
pub access_token: Option<String>,
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
NotFound(PathBuf),
|
||||
Io(io::Error),
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
impl From<io::Error> for ConfigError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Self::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config_path() -> Option<PathBuf> {
|
||||
paths::app_config_dir().map(|d| d.join("config.toml"))
|
||||
}
|
||||
|
||||
// Very small TOML-ish parser for now (no deps).
|
||||
// Format:
|
||||
// base_url = "http://..."
|
||||
// access_token = "..."
|
||||
pub fn load_config() -> Result<Config, ConfigError> {
|
||||
let path = config_path().ok_or_else(|| ConfigError::Parse("No HOME/XDG dirs".into()))?;
|
||||
if !path.exists() {
|
||||
return Err(ConfigError::NotFound(path));
|
||||
}
|
||||
|
||||
let s = fs::read_to_string(&path)?;
|
||||
parse_config(&s).map_err(ConfigError::Parse)
|
||||
}
|
||||
|
||||
fn parse_config(s: &str) -> Result<Config, String> {
|
||||
let mut base_url: Option<String> = None;
|
||||
let mut access_token: Option<String> = None;
|
||||
let mut user_id: Option<String> = None;
|
||||
|
||||
for (i, line) in s.lines().enumerate() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (k, v) = line
|
||||
.split_once('=')
|
||||
.ok_or_else(|| format!("Line {}: expected key = value", i + 1))?;
|
||||
|
||||
let key = k.trim();
|
||||
let mut val = v.trim().to_string();
|
||||
|
||||
// strip quotes if present
|
||||
if val.starts_with('"') && val.ends_with('"') && val.len() >= 2 {
|
||||
val = val[1..val.len() - 1].to_string();
|
||||
}
|
||||
|
||||
match key {
|
||||
"base_url" => base_url = Some(val),
|
||||
"access_token" => access_token = Some(val),
|
||||
"user_id" => user_id = Some(val),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let base_url = base_url.ok_or_else(|| "Missing base_url".to_string())?;
|
||||
|
||||
Ok(Config {
|
||||
base_url,
|
||||
access_token,
|
||||
user_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save_config(cfg: &Config) -> Result<(), ConfigError> {
|
||||
let dir = paths::app_config_dir()
|
||||
.ok_or_else(|| ConfigError::Parse("No HOME/XDG dirs".into()))?;
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
|
||||
let path = dir.join("config.toml");
|
||||
|
||||
let mut out = String::new();
|
||||
|
||||
out.push_str(&format!("base_url = \"{}\"\n", cfg.base_url));
|
||||
|
||||
if let Some(t) = &cfg.access_token {
|
||||
out.push_str(&format!("access_token = \"{}\"\n", t));
|
||||
}
|
||||
|
||||
if let Some(u) = &cfg.user_id {
|
||||
out.push_str(&format!("user_id = \"{}\"\n", u));
|
||||
}
|
||||
|
||||
std::fs::write(path, out)?;
|
||||
Ok(())
|
||||
}
|
||||
use std::{fs, io, path::PathBuf};
|
||||
|
||||
use super::paths;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub base_url: String,
|
||||
pub access_token: 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.
|
||||
pub device_id: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigError {
|
||||
NotFound(PathBuf),
|
||||
Io(io::Error),
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
impl From<io::Error> for ConfigError {
|
||||
fn from(e: io::Error) -> Self {
|
||||
Self::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config_path() -> Option<PathBuf> {
|
||||
paths::app_config_dir().map(|d| d.join("config.toml"))
|
||||
}
|
||||
|
||||
// Very small TOML-ish parser for now (no deps).
|
||||
// Format:
|
||||
// base_url = "http://..."
|
||||
// access_token = "..."
|
||||
pub fn load_config() -> Result<Config, ConfigError> {
|
||||
let path = config_path().ok_or_else(|| ConfigError::Parse("No HOME/XDG dirs".into()))?;
|
||||
if !path.exists() {
|
||||
return Err(ConfigError::NotFound(path));
|
||||
}
|
||||
|
||||
let s = fs::read_to_string(&path)?;
|
||||
parse_config(&s).map_err(ConfigError::Parse)
|
||||
}
|
||||
|
||||
fn parse_config(s: &str) -> Result<Config, String> {
|
||||
let mut base_url: Option<String> = None;
|
||||
let mut access_token: Option<String> = None;
|
||||
let mut user_id: Option<String> = None;
|
||||
let mut device_id: Option<String> = None;
|
||||
|
||||
for (i, line) in s.lines().enumerate() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (k, v) = line
|
||||
.split_once('=')
|
||||
.ok_or_else(|| format!("Line {}: expected key = value", i + 1))?;
|
||||
|
||||
let key = k.trim();
|
||||
let mut val = v.trim().to_string();
|
||||
|
||||
// strip quotes if present
|
||||
if val.starts_with('"') && val.ends_with('"') && val.len() >= 2 {
|
||||
val = val[1..val.len() - 1].to_string();
|
||||
}
|
||||
|
||||
match key {
|
||||
"base_url" => base_url = Some(val),
|
||||
"access_token" => access_token = Some(val),
|
||||
"user_id" => user_id = Some(val),
|
||||
"device_id" => device_id = Some(val),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let base_url = base_url.ok_or_else(|| "Missing base_url".to_string())?;
|
||||
|
||||
Ok(Config {
|
||||
base_url,
|
||||
access_token,
|
||||
user_id,
|
||||
device_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save_config(cfg: &Config) -> Result<(), ConfigError> {
|
||||
let dir = paths::app_config_dir()
|
||||
.ok_or_else(|| ConfigError::Parse("No HOME/XDG dirs".into()))?;
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
|
||||
let path = dir.join("config.toml");
|
||||
|
||||
let mut out = String::new();
|
||||
|
||||
out.push_str(&format!("base_url = \"{}\"\n", cfg.base_url));
|
||||
|
||||
if let Some(t) = &cfg.access_token {
|
||||
out.push_str(&format!("access_token = \"{}\"\n", t));
|
||||
}
|
||||
|
||||
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 jellyfin;
|
||||
pub mod paths;
|
||||
pub mod config;
|
||||
pub mod jellyfin;
|
||||
pub mod paths;
|
||||
pub mod downloads;
|
||||
+44
-44
@@ -1,44 +1,44 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
env::var_os("HOME").map(PathBuf::from)
|
||||
}
|
||||
|
||||
pub fn config_dir() -> Option<PathBuf> {
|
||||
// XDG first, fallback to ~/.config
|
||||
if let Some(p) = env::var_os("XDG_CONFIG_HOME").map(PathBuf::from) {
|
||||
Some(p)
|
||||
} else {
|
||||
home_dir().map(|h| h.join(".config"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_dir() -> Option<PathBuf> {
|
||||
// XDG first, fallback to ~/.cache
|
||||
if let Some(p) = env::var_os("XDG_CACHE_HOME").map(PathBuf::from) {
|
||||
Some(p)
|
||||
} else {
|
||||
home_dir().map(|h| h.join(".cache"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_config_dir() -> Option<PathBuf> {
|
||||
config_dir().map(|d| d.join("cjtui"))
|
||||
}
|
||||
|
||||
pub fn app_cache_dir() -> Option<PathBuf> {
|
||||
cache_dir().map(|d| d.join("cjtui"))
|
||||
}
|
||||
|
||||
pub fn posters_cache_dir() -> Option<PathBuf> {
|
||||
app_cache_dir().map(|d| d.join("posters"))
|
||||
}
|
||||
|
||||
pub fn downloads_dir() -> Option<PathBuf> {
|
||||
app_config_dir().map(|d| d.join("downloads"))
|
||||
}
|
||||
|
||||
pub fn temp_downloads_dir() -> Option<PathBuf> {
|
||||
app_config_dir().map(|d| d.join("temp"))
|
||||
}
|
||||
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
env::var_os("HOME").map(PathBuf::from)
|
||||
}
|
||||
|
||||
pub fn config_dir() -> Option<PathBuf> {
|
||||
// XDG first, fallback to ~/.config
|
||||
if let Some(p) = env::var_os("XDG_CONFIG_HOME").map(PathBuf::from) {
|
||||
Some(p)
|
||||
} else {
|
||||
home_dir().map(|h| h.join(".config"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_dir() -> Option<PathBuf> {
|
||||
// XDG first, fallback to ~/.cache
|
||||
if let Some(p) = env::var_os("XDG_CACHE_HOME").map(PathBuf::from) {
|
||||
Some(p)
|
||||
} else {
|
||||
home_dir().map(|h| h.join(".cache"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app_config_dir() -> Option<PathBuf> {
|
||||
config_dir().map(|d| d.join("cjtui"))
|
||||
}
|
||||
|
||||
pub fn app_cache_dir() -> Option<PathBuf> {
|
||||
cache_dir().map(|d| d.join("cjtui"))
|
||||
}
|
||||
|
||||
pub fn posters_cache_dir() -> Option<PathBuf> {
|
||||
app_cache_dir().map(|d| d.join("posters"))
|
||||
}
|
||||
|
||||
pub fn downloads_dir() -> Option<PathBuf> {
|
||||
app_config_dir().map(|d| d.join("downloads"))
|
||||
}
|
||||
|
||||
pub fn temp_downloads_dir() -> Option<PathBuf> {
|
||||
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