title modification
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user