返回 douyin-downloader
test_naming.py
根目录 / tests / test_naming.py
1 """Tests for utils.naming — custom filename/folder templates.
2
3 Validates the render + validation contract used by both the downloaders
4 (BaseDownloader._download_aweme_assets, MusicDownloader, LiveDownloader) and
5 the desktop settings API (PATCH /api/v1/settings).
6 """
7
8 from __future__ import annotations
9
10 from datetime import datetime
11
12 import pytest
13
14 from utils.naming import (
15 ALLOWED_VARIABLES,
16 DEFAULT_FILE_TEMPLATE,
17 DEFAULT_FOLDER_TEMPLATE,
18 MAX_TEMPLATE_LENGTH,
19 TemplateValidationError,
20 build_aweme_context,
21 build_live_context,
22 build_music_context,
23 render_template,
24 validate_template,
25 )
26
27 # ---------------------------------------------------------------------------
28 # validate_template
29 # ---------------------------------------------------------------------------
30
31
32 def test_validate_template_accepts_defaults():
33 validate_template(DEFAULT_FILE_TEMPLATE)
34 validate_template(DEFAULT_FOLDER_TEMPLATE)
35
36
37 @pytest.mark.parametrize(
38 "bad,needle",
39 [
40 ("", "empty"),
41 (" ", "empty"),
42 ("a" * (MAX_TEMPLATE_LENGTH + 1) + "{id}", "<="),
43 ("foo/bar_{id}", "path separators"),
44 ("foo\\bar_{id}", "path separators"),
45 ("{date}_{title}", "{id}"),
46 ("{unknown}_{id}", "unknown"),
47 ("static_prefix", "at least one variable"),
48 ],
49 )
50 def test_validate_template_rejects(bad: str, needle: str):
51 with pytest.raises(TemplateValidationError) as exc:
52 validate_template(bad)
53 assert needle in str(exc.value)
54
55
56 def test_validate_template_uses_field_name_in_error():
57 with pytest.raises(TemplateValidationError) as exc:
58 validate_template("{date}", field_name="filename_template")
59 assert "filename_template" in str(exc.value)
60
61
62 # ---------------------------------------------------------------------------
63 # render_template
64 # ---------------------------------------------------------------------------
65
66
67 def test_render_template_replaces_all_known_vars():
68 ctx = {v: v for v in ALLOWED_VARIABLES}
69 # Use a minimal template to stay under sanitize_filename's 80-char cap.
70 tpl = "{id}_{title}_{author}_{date}_{type}_{mode}"
71 out = render_template(tpl, ctx)
72 for v in ("id", "title", "author", "date", "type", "mode"):
73 assert v in out
74
75
76 def test_render_template_unknown_keys_render_as_empty():
77 out = render_template("{title}_{totally_unknown}_{id}", {"title": "hi", "id": "42"})
78 assert out == "hi_42"
79
80
81 def test_render_template_missing_context_value_renders_empty():
82 out = render_template("{date}_{title}_{id}", {"date": "", "title": "", "id": "42"})
83 assert out == "42"
84
85
86 def test_render_template_falls_back_when_result_blank():
87 out = render_template("{title}", {"title": ""}, fallback="2024-01-01_42")
88 assert out == "2024-01-01_42"
89
90
91 def test_render_template_sanitizes_illegal_chars():
92 out = render_template(
93 "{date}_{title}_{id}",
94 {"date": "2024-01-01", "title": "bad/name?*", "id": "42"},
95 )
96 # Slashes, stars, question marks collapse into underscores via
97 # sanitize_filename, then consecutive underscores collapse.
98 assert "/" not in out and "?" not in out and "*" not in out
99 assert "42" in out
100 assert "2024-01-01" in out
101
102
103 # ---------------------------------------------------------------------------
104 # context builders
105 # ---------------------------------------------------------------------------
106
107
108 def test_build_aweme_context_minimum_fields():
109 ts = int(datetime(2024, 3, 15, 18, 30).timestamp())
110 ctx = build_aweme_context(
111 aweme_id="7412345678901234567",
112 title="山里的秋天",
113 author_name="某作者",
114 author_sec_uid="MS4wLjABAAA",
115 publish_date="2024-03-15",
116 publish_ts=ts,
117 media_type="video",
118 mode="post",
119 )
120 assert ctx["id"] == "7412345678901234567"
121 assert ctx["title"] == "山里的秋天"
122 assert ctx["author"] == "某作者"
123 assert ctx["author_id"] == "MS4wLjABAAA"
124 assert ctx["date"] == "2024-03-15"
125 assert ctx["year"] == "2024"
126 assert ctx["month"] == "03"
127 assert ctx["day"] == "15"
128 assert ctx["type"] == "video"
129 assert ctx["mode"] == "post"
130 assert ctx["time"] == "1830"
131 assert ctx["timestamp"] == str(ts)
132
133
134 def test_build_aweme_context_defaults_title_when_blank():
135 ctx = build_aweme_context(
136 aweme_id="42",
137 title="",
138 author_name="a",
139 author_sec_uid=None,
140 publish_date="2024-01-01",
141 publish_ts=None,
142 media_type="video",
143 )
144 assert ctx["title"] == "no_title"
145 assert ctx["author_id"] == ""
146 assert ctx["timestamp"] == ""
147 assert ctx["time"] == ""
148
149
150 def test_build_music_context_prefixes_music_id():
151 ctx = build_music_context(
152 music_id="999",
153 title="某 BGM",
154 author_name="作曲人",
155 publish_date="2024-01-02",
156 )
157 assert ctx["id"] == "music_999"
158 assert ctx["type"] == "music"
159 assert ctx["date"] == "2024-01-02"
160
161
162 def test_build_live_context_sets_time():
163 started_at = datetime(2024, 5, 10, 21, 3, 45)
164 ctx = build_live_context(
165 room_id="7400000000000000000",
166 title="直播中",
167 author_name="主播",
168 started_at=started_at,
169 )
170 assert ctx["id"] == "7400000000000000000"
171 # `date` intentionally includes HHMM for live streams so the default
172 # template preserves the legacy `YYYY-MM-DD_HHMM_{title}_{id}` layout.
173 assert ctx["date"] == "2024-05-10_2103"
174 assert ctx["year"] == "2024"
175 assert ctx["month"] == "05"
176 assert ctx["day"] == "10"
177 assert ctx["time"] == "2103"
178 assert ctx["type"] == "live"
179 assert ctx["timestamp"] == str(int(started_at.timestamp()))
180
181
182 # ---------------------------------------------------------------------------
183 # end-to-end render via the default template
184 # ---------------------------------------------------------------------------
185
186
187 def test_default_template_matches_legacy_layout():
188 """The default template must produce output identical to the pre-template
189 f-string (minus sanitize_filename collapsing). This is the compatibility
190 anchor: users who never touch the setting keep the exact same paths.
191 """
192 ctx = build_aweme_context(
193 aweme_id="7412345678901234567",
194 title="今天去爬山啦",
195 author_name="ignored_here",
196 author_sec_uid=None,
197 publish_date="2026-04-10",
198 publish_ts=None,
199 media_type="video",
200 mode="post",
201 )
202 assert render_template(DEFAULT_FILE_TEMPLATE, ctx) == (
203 "2026-04-10_今天去爬山啦_7412345678901234567"
204 )
205
205 lines PYTHON