Initial commit: modular Streamlit media viewer with Jellyfin, SSH tools, and docs
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""Selected-file preview and remote path tools UI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
import pandas as pd
|
||||
import streamlit as st
|
||||
|
||||
from media_library_viewer.jobs import JOB_TEMPLATES, run_job
|
||||
from media_library_viewer.utils import (
|
||||
ffprobe_format_summary,
|
||||
is_known_video_file,
|
||||
summarize_audio_streams,
|
||||
summarize_streams,
|
||||
summarize_subtitle_streams,
|
||||
summarize_video_streams,
|
||||
)
|
||||
|
||||
|
||||
def render_ffprobe_sections(ffprobe_data: dict[str, Any]) -> None:
|
||||
"""Render ffprobe output in separate container/video/audio/subtitle sections."""
|
||||
format_summary = ffprobe_format_summary(ffprobe_data)
|
||||
video_rows = summarize_video_streams(ffprobe_data)
|
||||
audio_rows = summarize_audio_streams(ffprobe_data)
|
||||
subtitle_rows = summarize_subtitle_streams(ffprobe_data)
|
||||
|
||||
st.markdown("**Container**")
|
||||
st.dataframe(pd.DataFrame([format_summary]), use_container_width=True, hide_index=True)
|
||||
|
||||
st.markdown("**Video**")
|
||||
if video_rows:
|
||||
st.dataframe(pd.DataFrame(video_rows), use_container_width=True, hide_index=True)
|
||||
else:
|
||||
st.caption("No video streams found.")
|
||||
|
||||
st.markdown("**Audio**")
|
||||
if audio_rows:
|
||||
st.dataframe(pd.DataFrame(audio_rows), use_container_width=True, hide_index=True)
|
||||
else:
|
||||
st.caption("No audio streams found.")
|
||||
|
||||
st.markdown("**Subtitles**")
|
||||
if subtitle_rows:
|
||||
st.dataframe(pd.DataFrame(subtitle_rows), use_container_width=True, hide_index=True)
|
||||
else:
|
||||
st.caption("No subtitle streams found.")
|
||||
|
||||
|
||||
def render_selected_file_preview(
|
||||
ssh_args: tuple,
|
||||
selected_path: str | None,
|
||||
cached_ffprobe_preview: Callable[..., dict[str, Any]],
|
||||
) -> None:
|
||||
"""Run and render a blocking ffprobe preview for selected known video files."""
|
||||
with st.container(border=True):
|
||||
st.markdown("**Selected file preview**")
|
||||
if not selected_path:
|
||||
st.caption("Select a file to preview media metadata.")
|
||||
return
|
||||
|
||||
st.caption(f"Path: `{selected_path}`")
|
||||
if not is_known_video_file(selected_path):
|
||||
st.caption("Automatic ffprobe preview runs for known video file extensions only.")
|
||||
return
|
||||
|
||||
refresh_col, status_col = st.columns([1.2, 5])
|
||||
if refresh_col.button("Reload preview", key="preview_reload", use_container_width=True):
|
||||
cached_ffprobe_preview.clear()
|
||||
st.rerun()
|
||||
|
||||
host, username, port, key_filename, password = ssh_args
|
||||
try:
|
||||
with st.spinner("Running ffprobe preview..."):
|
||||
ffprobe_data = cached_ffprobe_preview(host, username, port, key_filename, password, selected_path)
|
||||
except Exception as exc:
|
||||
status_col.error(f"ffprobe failed: {exc}")
|
||||
return
|
||||
|
||||
status_col.success("ffprobe preview loaded.")
|
||||
render_ffprobe_sections(ffprobe_data)
|
||||
with st.expander("Raw ffprobe JSON"):
|
||||
st.json(ffprobe_data)
|
||||
|
||||
|
||||
def render_ssh_tools(
|
||||
ssh,
|
||||
ssh_args: tuple,
|
||||
selected_path: str | None,
|
||||
cached_ffprobe_preview: Callable[..., dict[str, Any]],
|
||||
) -> None:
|
||||
"""Render selected-path diagnostics and safe job templates."""
|
||||
render_selected_file_preview(ssh_args, selected_path, cached_ffprobe_preview)
|
||||
if not selected_path:
|
||||
return
|
||||
|
||||
st.subheader("Disk metadata and jobs")
|
||||
tabs = st.tabs(["ffprobe", "stat", "jobs"])
|
||||
|
||||
with tabs[0]:
|
||||
if st.button("Run ffprobe on selected path", key="tools_run_ffprobe"):
|
||||
try:
|
||||
data = ssh.ffprobe_json(selected_path)
|
||||
render_ffprobe_sections(data)
|
||||
with st.expander("All streams table"):
|
||||
st.dataframe(pd.DataFrame(summarize_streams(data)), use_container_width=True, hide_index=True)
|
||||
with st.expander("Raw ffprobe JSON"):
|
||||
st.json(data)
|
||||
except Exception as exc:
|
||||
st.error(str(exc))
|
||||
|
||||
with tabs[1]:
|
||||
if st.button("Run stat", key="tools_run_stat"):
|
||||
result = ssh.stat_path(selected_path)
|
||||
st.code(result.stdout or result.stderr)
|
||||
|
||||
with tabs[2]:
|
||||
st.warning("Jobs run commands on the remote server. Phase 1 includes safe/read-only templates only.")
|
||||
job_key = st.selectbox("Job", list(JOB_TEMPLATES.keys()), format_func=lambda k: JOB_TEMPLATES[k].name)
|
||||
st.caption(JOB_TEMPLATES[job_key].description)
|
||||
command_preview = JOB_TEMPLATES[job_key].render({"path": selected_path})
|
||||
st.code(command_preview, language="bash")
|
||||
if st.button("Run selected job", key="tools_run_selected_job"):
|
||||
result = run_job(ssh, job_key, selected_path)
|
||||
st.write(f"Exit status: `{result.exit_status}`")
|
||||
if result.stdout:
|
||||
st.code(result.stdout)
|
||||
if result.stderr:
|
||||
st.error(result.stderr)
|
||||
Reference in New Issue
Block a user