title modification

This commit is contained in:
2026-09-26 08:03:20 +08:00
parent 37ee850ee1
commit 7fae2bb8c9
6 changed files with 106 additions and 1 deletions

View File

@@ -39,6 +39,7 @@ from .models import (
AuthRequest,
AuthResponse,
CourseSummary,
CourseTitleUpdate,
SentenceBoundaryAdjust,
SentenceBoundary,
SentenceBoundaryDocument,
@@ -624,6 +625,20 @@ def create_app(
background_tasks.add_task(_process_safely, processor, video_hash)
return {"video_hash": video_hash.lower(), "status": "processing_queued"}
@application.put(
"/api/v1/admin/videos/{video_hash}/title",
response_model=VideoSummary,
dependencies=[Depends(require_admin)],
)
def update_course_title(
payload: CourseTitleUpdate,
video_hash: str = SHA256_PATH,
) -> VideoSummary:
video = video_repository.update_title(video_hash, payload.title)
if video is None:
raise HTTPException(status_code=404, detail="Video was not found.")
return _video_summary(video, service_settings)
@application.put(
"/api/v1/admin/videos/{video_hash}/sentences/{sentence_index}",
response_model=SentenceBoundary,

View File

@@ -92,6 +92,20 @@ class SentenceTextUpdate(BaseModel):
language: Optional[str] = Field(default=None, max_length=32)
class CourseTitleUpdate(BaseModel):
model_config = ConfigDict(extra="forbid")
title: str = Field(max_length=200)
@field_validator("title")
@classmethod
def validate_title(cls, value: str) -> str:
cleaned = value.strip()
if not cleaned:
raise ValueError("title cannot be empty")
return cleaned
class PasswordResetRequest(BaseModel):
model_config = ConfigDict(extra="forbid")

View File

@@ -349,6 +349,22 @@ class VideoRepository:
(remote_url, utc_now(), normalize_video_hash(video_hash)),
)
def update_title(self, video_hash: str, title: str) -> Optional[Dict[str, Any]]:
normalized_hash = normalize_video_hash(video_hash)
cleaned_title = title.strip()[:200]
if not cleaned_title:
raise ValueError("Title cannot be empty.")
with self._connect() as connection:
cursor = connection.execute(
"UPDATE videos SET title = ?, updated_at = ? WHERE video_hash = ?",
(cleaned_title, utc_now(), normalized_hash),
)
if cursor.rowcount == 0:
return None
return self.get_video(normalized_hash)
def list_videos(self) -> List[Dict[str, Any]]:
with self._connect() as connection:
rows = connection.execute(

View File

@@ -114,6 +114,6 @@
</div>
</dialog>
<script src="/static/admin.js?v=admin-merge-sentence-v1" defer></script>
<script src="/static/admin.js?v=admin-course-title-v1" defer></script>
</body>
</html>

View File

@@ -181,6 +181,7 @@ function actionCell(video) {
play.rel = "noopener";
play.textContent = "播放";
actions.append(play);
actions.append(button("改标题", () => renameVideo(video)));
actions.append(button("句子", () => openSentences(video)));
actions.append(button("重新处理", () => reprocess(video.video_hash)));
const remove = button("删除", () => deleteVideo(video));
@@ -198,6 +199,28 @@ function button(label, handler) {
return result;
}
async function renameVideo(video) {
const title = window.prompt("请输入新的课程标题", video.title || "");
if (title === null) return;
const cleanedTitle = title.trim();
if (!cleanedTitle) {
elements.serviceState.textContent = "课程标题不能为空";
return;
}
if (cleanedTitle === video.title) return;
try {
await api(`/api/v1/admin/videos/${video.video_hash}/title`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: cleanedTitle }),
});
elements.serviceState.textContent = "课程标题已更新";
await loadVideos();
} catch (error) {
elements.serviceState.textContent = `标题修改失败: ${error.message}`;
}
}
function uploadVideo(event) {
event.preventDefault();
const file = document.querySelector("#video-file").files[0];

View File

@@ -562,3 +562,40 @@ def test_admin_boundary_adjustment_updates_linked_next_start(tmp_path):
document = repository.get_document(VIDEO_HASH)
assert [item.end_ms for item in document.sentences] == [4250, 9000]
assert [item.start_ms for item in document.sentences] == [0, 4250]
def test_admin_can_update_course_title(tmp_path):
settings = replace(Settings.from_env(), data_dir=tmp_path / "data")
settings.ensure_directories()
repository = VideoRepository(settings.database_path)
repository.upsert_upload(
video_hash=VIDEO_HASH,
title="Old course title",
filename="lesson.mp4",
stored_filename=f"{VIDEO_HASH}.mp4",
content_type="video/mp4",
size_bytes=5,
language="en",
)
client = TestClient(
create_app(
BoundaryStore(tmp_path / "boundaries.json"),
settings=settings,
repository=repository,
)
)
response = client.put(
f"/api/v1/admin/videos/{VIDEO_HASH}/title",
json={"title": " New course title "},
)
assert response.status_code == 200
assert response.json()["title"] == "New course title"
assert repository.get_video(VIDEO_HASH)["title"] == "New course title"
missing_response = client.put(
f"/api/v1/admin/videos/{'b' * 64}/title",
json={"title": "Missing course"},
)
assert missing_response.status_code == 404