Skip to content

Commit 223ad38

Browse files
committed
Added new tui/web interface for viewing current jobs. Will implement input script later.
1 parent d636d1a commit 223ad38

File tree

14 files changed

+465
-144
lines changed

14 files changed

+465
-144
lines changed

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,22 @@ TMDB_API_KEY=putyourkeyhere
4545

4646
Set input and output paths in config.json
4747

48-
Navigate up to your media directory. Activate the venv and run the input script.
48+
Navigate to the shipper directory. Activate the venv and run the input script.
4949
```sh
50-
source shipper/venv/bin/activate
51-
python shipper/input.py
50+
source venv/bin/activate
51+
python input.py
52+
```
53+
54+
Once you have added an input you can track it either in the terminal or on a website.
55+
56+
**Terminal:**
57+
```sh
58+
python tui.py
59+
```
60+
61+
**Website: (http://localhost:8932/)
62+
```sh
63+
python tui_server.py
5264
```
5365

5466

TODO.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,25 @@
33
There is still lots to add. If there are any features you think should be added, feel free to make an issue.
44

55
- [ ] Add mode just to copy to correct names (encode quality original)
6-
7-
- [x] Reorganise file structure to move functions to their own files.
8-
- [x] daemon
9-
- [x] input
10-
- [x] status
6+
- [ ] Add restart flag for restarting daemon
7+
- [ ] Add flag to clear finished jobs / series
118

129
- [ ] input.py
10+
- [ ] MOVE TO TUI
1311
- [ ] Allow custom output file name formats
1412
- [ ] Fix ctrl+c throwing 20 lines of error
15-
- [ ] Potentially make part of status.py TUI
1613
- [ ] status.py
17-
- [ ] Consider moving to TUI frontend such as textualize which would allow it to run as webpage.
14+
- [x] Consider moving to TUI frontend such as textualize which would allow it to run as webpage.
1815
- [ ] Running status or input should trigger daemon to start.
19-
- [x] daemon.py
20-
- [x] Integrate new compression integration
21-
- [x] Move all ffmpeg to using external functions
16+
17+
- [ ] tui.py
18+
- [ ] Add input
19+
- [ ] Add config and manual pages
20+
- [ ] Allow reorganising of jobs
2221

2322
- [ ] compression script
2423
- [ ] Warn if output > input
2524

2625
- [ ] Prompt user for missing config.json & .env values when initially running input.py.
2726
- [ ] Allow editing of current jobs.
28-
- [ ] Add help/info command
2927
- [ ] `scp` jobs for automatically moving files to a different device.

config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
"encode": 1,
77
"frame_count": 2
88
},
9+
"site": {
10+
"host": "localhost",
11+
"port": 8932
12+
},
913
"quality_presets": {
1014
"very_low": {
1115
"crf": "28",

css/textual.tcss

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Screen {
2+
align: center middle;
3+
padding: 1;
4+
}
5+
6+
#buttons {
7+
height: 3;
8+
width: auto;
9+
}
10+
11+
ContentSwitcher {
12+
border: round $primary;
13+
width: 90%;
14+
height: 1fr;
15+
}
16+
17+
MarkdownH2 {
18+
background: $panel;
19+
color: yellow;
20+
border: none;
21+
padding: 0 1;
22+
}

functions/command_runner.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ def run_ffmpeg_encode(
139139
break
140140

141141
if line:
142-
print(line)
143142
try:
144143
match = re.search(r'frame=\s*(\d+)', line)
145144
if match:

functions/config.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
PROJECT_ROOT = Path(__file__).resolve().parent.parent
88
CONFIG_FILE_PATH = PROJECT_ROOT / "config.json"
99
DOTENV_PATH = PROJECT_ROOT / ".env"
10+
DATA_FILE_PATH = PROJECT_ROOT / "data.json"
1011

1112
# local config files that are not pushed to github
1213
LOCAL_CONFIG_FILE_PATH = PROJECT_ROOT / "config.local.json"
@@ -48,6 +49,24 @@ def __init__(self, raw_config: Dict[str, Any]):
4849
# --- 5. Storage Buffer ---
4950
self.buffer: float = raw_config.get('storage_buffer', 10)
5051

52+
# --- 6. Textual Table Columns
53+
self.textual_columns: list = raw_config.get('textual_columns', [
54+
"name",
55+
"status",
56+
"percentage",
57+
"eta"
58+
])
59+
self.textual_overview_columns: list = raw_config.get(
60+
'textual_overview_columns', [
61+
"name",
62+
"progress",
63+
"status"
64+
]
65+
)
66+
67+
# --- 7. Site settings ---
68+
self.site: list = raw_config.get("site", ["localhost", 8932]).values()
69+
5170

5271
def load_config() -> Config:
5372
"""

functions/t_info.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from textual.widgets import ProgressBar
2+
from .disk_stats import get_disk_metrics
3+
4+
5+
class DiskSpaceBar(ProgressBar):
6+
def check_space(self) -> None:
7+
total, used, pct = get_disk_metrics()
8+
self.update(
9+
total=total,
10+
progress=used
11+
)
12+
13+
def on_mount(self) -> None:
14+
"""Called when widget first attached."""
15+
16+
self.check_space()
17+
18+
self.set_interval(15, self.check_space)

functions/t_status.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import json
2+
import os
3+
from textual.widgets import DataTable
4+
5+
from .config import DATA_FILE_PATH, load_config
6+
from .t_status_data import get_data_table, get_overview_data_table
7+
8+
CONFIG = load_config()
9+
10+
11+
class StatusTable(DataTable):
12+
"""A custom widget for displaying data from a dynamically updating JSON file."""
13+
14+
# Store the last known modification time of the file
15+
last_mtime: float = 0.0
16+
17+
def load_data_from_json(self) -> None:
18+
"""Reads and updates the table content from the JSON file."""
19+
try:
20+
# Check if file has been modified recently
21+
current_mtime = os.path.getmtime(DATA_FILE_PATH)
22+
if current_mtime <= self.last_mtime:
23+
return # Skip if not modified
24+
self.last_mtime = current_mtime
25+
26+
# Get new data
27+
with open(DATA_FILE_PATH, "r") as f:
28+
try:
29+
data = json.load(f)
30+
except Exception:
31+
return
32+
if not data:
33+
return # Skip if data is empty
34+
filtered_data = get_data_table(data)
35+
36+
# Add new data
37+
data_uids = filtered_data.keys()
38+
current_table_uids = self.rows.keys()
39+
uids_to_add = [i for i in data_uids if i not in current_table_uids]
40+
for uid in uids_to_add:
41+
self.add_row(*filtered_data[uid].values(), key=uid, label=uid)
42+
43+
# Remove old data
44+
uids_to_remove = [
45+
i for i in current_table_uids if i not in data_uids]
46+
for uid in uids_to_remove:
47+
self.remove_row(uid)
48+
49+
# Edit changed data
50+
uids_to_edit = [
51+
i for i in data_uids
52+
if i not in uids_to_add
53+
and i not in uids_to_remove
54+
]
55+
for uid in uids_to_edit:
56+
for c_no in range(len(CONFIG.textual_columns)):
57+
self.update_cell(
58+
uid,
59+
str(c_no),
60+
filtered_data[uid][CONFIG.textual_columns[c_no]]
61+
)
62+
63+
except FileNotFoundError:
64+
# Optional: Display a message if the file is missing
65+
self.app.log(f"JSON file not found: {DATA_FILE_PATH}")
66+
except Exception as e:
67+
self.app.log(f"Error loading JSON data: {e}")
68+
69+
def on_mount(self) -> None:
70+
"""Called when the widget is first attached to the app."""
71+
72+
columns = CONFIG.textual_columns
73+
for c_no in range(len(columns)):
74+
self.add_column(columns[c_no], key=str(c_no))
75+
76+
self.load_data_from_json()
77+
78+
self.set_interval(1, self.load_data_from_json)
79+
80+
81+
class OverviewTable(DataTable):
82+
"""A custom widget for displaying data from a dynamically updating JSON file."""
83+
84+
# Store the last known modification time of the file
85+
last_mtime: float = 0.0
86+
87+
def load_data_from_json(self) -> None:
88+
"""Reads and updates the table content from the JSON file."""
89+
# try:
90+
# Check if file has been modified recently
91+
current_mtime = os.path.getmtime(DATA_FILE_PATH)
92+
if current_mtime <= self.last_mtime:
93+
return # Skip if not modified
94+
self.last_mtime = current_mtime
95+
96+
# Get new data
97+
with open(DATA_FILE_PATH, "r") as f:
98+
try:
99+
data = json.load(f)
100+
except Exception:
101+
return
102+
if not data:
103+
return # Skip if data is empty
104+
filtered_data = get_overview_data_table(data)
105+
106+
# Add new data
107+
data_uids = filtered_data.keys()
108+
current_table_uids = self.rows.keys()
109+
uids_to_add = [i for i in data_uids if i not in current_table_uids]
110+
for uid in uids_to_add:
111+
self.add_row(*filtered_data[uid].values(), key=uid, label=uid)
112+
113+
# Remove old data
114+
uids_to_remove = [
115+
i for i in current_table_uids if i not in data_uids]
116+
for uid in uids_to_remove:
117+
self.remove_row(uid)
118+
119+
# Edit changed data
120+
uids_to_edit = [
121+
i for i in data_uids
122+
if i not in uids_to_add
123+
and i not in uids_to_remove
124+
]
125+
for uid in uids_to_edit:
126+
for c_no in range(len(CONFIG.textual_overview_columns)):
127+
self.update_cell(
128+
uid,
129+
str(c_no),
130+
filtered_data[uid][
131+
CONFIG.textual_overview_columns[c_no]]
132+
)
133+
134+
# except FileNotFoundError:
135+
# # Optional: Display a message if the file is missing
136+
# self.app.log(f"JSON file not found: {DATA_FILE_PATH}")
137+
# except Exception as e:
138+
# self.app.log(f"Error loading JSON data: {e}")
139+
140+
def on_mount(self) -> None:
141+
"""Called when the widget is first attached to the app."""
142+
143+
columns = CONFIG.textual_overview_columns
144+
for c_no in range(len(columns)):
145+
self.add_column(columns[c_no], key=str(c_no))
146+
147+
self.load_data_from_json()
148+
149+
self.set_interval(1, self.load_data_from_json)

0 commit comments

Comments
 (0)