Skip to content

Commit b86169d

Browse files
ChanMoclaude
andcommitted
release: v0.8.17 Feed主题条、图片组查看器与来源分组
- 新增 Feed 主题条(theme strip)功能,按来源分组展示相关媒体 - 新增 Feed 图片组(image group)内联查看器,支持滑动切换 - 后端新增按 source_url / job_id 聚合媒体分组逻辑 - Library 页面支持 ?focus=name 参数,自动定位并打开指定项目 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a932df9 commit b86169d

5 files changed

Lines changed: 979 additions & 28 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "TikLocal"
3-
version = "0.8.16"
3+
version = "0.8.17"
44
description = "A local media server that combines the features of TikTok and Pinterest"
55
authors = ["ChanMo <chan.mo@outlook.com>"]
66
readme = "README.md"

tests/test_feed_mix.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
13
import pytest
24

35
from tiklocal.app import create_app
@@ -72,3 +74,115 @@ def test_mix_feed_falls_back_to_videos_when_no_images(tmp_path, monkeypatch):
7274
items = data["items"]
7375
assert len(items) > 0
7476
assert all(item["type"] == "video" for item in items)
77+
78+
79+
def test_mix_feed_can_insert_theme_strip_for_recent_downloads_and_favorites(tmp_path, monkeypatch):
80+
media_root = tmp_path / "media"
81+
media_root.mkdir(parents=True, exist_ok=True)
82+
for name in ("v1.mp4", "v2.mp4", "i1.jpg", "i2.png", "i3.jpg"):
83+
(media_root / name).write_bytes(b"00")
84+
85+
(media_root / "favorite.json").write_text(
86+
json.dumps(["i1.jpg", "i2.png", "v1.mp4"]),
87+
encoding="utf-8",
88+
)
89+
90+
data_root = tmp_path / "tiklocal-data"
91+
data_root.mkdir(parents=True, exist_ok=True)
92+
(data_root / "download_jobs.json").write_text(
93+
json.dumps([
94+
{
95+
"id": "job-1",
96+
"status": "success",
97+
"created_at": "2026-03-14T10:00:00Z",
98+
"output_files_rel": ["i3.jpg", "v2.mp4", "i2.png"],
99+
}
100+
]),
101+
encoding="utf-8",
102+
)
103+
104+
monkeypatch.setenv("MEDIA_ROOT", str(media_root))
105+
monkeypatch.setenv("TIKLOCAL_INSTANCE", str(data_root))
106+
107+
app = create_app({"TESTING": True, "MEDIA_ROOT": media_root})
108+
test_client = app.test_client()
109+
110+
res = test_client.get("/api/feed/mix?page=1&size=24&seed=theme-seed")
111+
assert res.status_code == 200
112+
data = res.get_json()
113+
114+
items = data["items"]
115+
strip = next((item for item in items if item.get("type") == "theme_strip"), None)
116+
assert strip is not None
117+
assert strip["name"] in {"theme:recent-downloads", "theme:favorite-picks"}
118+
assert strip["title"]
119+
assert strip["target_url"] in {"/favorite", "/library"}
120+
assert strip["target_label"]
121+
assert len(strip["items"]) >= 3
122+
for child in strip["items"]:
123+
assert child["type"] in {"video", "image"}
124+
assert child["name"]
125+
assert child["media_url"]
126+
assert child["thumb_url"]
127+
assert child["detail_url"]
128+
assert child["focus_url"]
129+
130+
131+
def test_mix_feed_prefers_original_post_group_when_source_has_multiple_media(tmp_path, monkeypatch):
132+
media_root = tmp_path / "media"
133+
media_root.mkdir(parents=True, exist_ok=True)
134+
for name in ("set-1.jpg", "set-2.jpg", "solo.mp4", "fallback.jpg"):
135+
(media_root / name).write_bytes(b"00")
136+
137+
data_root = tmp_path / "tiklocal-data"
138+
data_root.mkdir(parents=True, exist_ok=True)
139+
(data_root / "download_sources.json").write_text(
140+
json.dumps(
141+
{
142+
"version": 1,
143+
"updated_at": "2026-03-14T10:30:00Z",
144+
"items": {
145+
"set-1.jpg": {
146+
"source_url_raw": "https://x.com/demo/status/100",
147+
"source_url_display": "https://x.com/demo/status/100",
148+
"source_domain": "x.com",
149+
"job_id": "job-group",
150+
"created_at": "2026-03-14T10:00:00Z",
151+
},
152+
"set-2.jpg": {
153+
"source_url_raw": "https://x.com/demo/status/100",
154+
"source_url_display": "https://x.com/demo/status/100",
155+
"source_domain": "x.com",
156+
"job_id": "job-group",
157+
"created_at": "2026-03-14T10:00:00Z",
158+
},
159+
"solo.mp4": {
160+
"source_url_raw": "https://x.com/demo/status/101",
161+
"source_url_display": "https://x.com/demo/status/101",
162+
"source_domain": "x.com",
163+
"job_id": "job-solo",
164+
"created_at": "2026-03-14T09:00:00Z",
165+
},
166+
},
167+
}
168+
),
169+
encoding="utf-8",
170+
)
171+
172+
monkeypatch.setenv("MEDIA_ROOT", str(media_root))
173+
monkeypatch.setenv("TIKLOCAL_INSTANCE", str(data_root))
174+
175+
app = create_app({"TESTING": True, "MEDIA_ROOT": media_root})
176+
test_client = app.test_client()
177+
178+
res = test_client.get("/api/feed/mix?page=1&size=24&seed=group-seed")
179+
assert res.status_code == 200
180+
data = res.get_json()
181+
182+
group = next((item for item in data["items"] if item.get("type") == "image_group"), None)
183+
assert group is not None
184+
assert group["title"] == "原始图集"
185+
names = [child["name"] for child in group["items"]]
186+
assert "set-1.jpg" in names
187+
assert "set-2.jpg" in names
188+
assert all(child["media_url"] for child in group["items"])

0 commit comments

Comments
 (0)