@@ -276,3 +276,103 @@ def test_delete_and_clear_history(client):
276276 assert clear_res .status_code == 200
277277 assert clear_data ["success" ] is True
278278 assert clear_data ["data" ]["deleted" ] >= 0
279+
280+
281+ def test_source_api_from_job_map (client ):
282+ res = client .post ("/api/download/jobs" , json = {"url" : "https://x.com/i/web/status/1234567890123456789?utm_source=test" })
283+ data = res .get_json ()
284+ assert res .status_code == 200
285+ job_id = data ["data" ]["job" ]["id" ]
286+ _wait_for_job (client , job_id )
287+
288+ source_res = client .get ("/api/source" , query_string = {"file" : "mock-output.mp4" })
289+ source_data = source_res .get_json ()
290+ assert source_res .status_code == 200
291+ assert source_data ["success" ] is True
292+ assert source_data ["data" ]["source" ]["resolved_by" ] == "map"
293+ assert source_data ["data" ]["source" ]["source_domain" ] == "x.com"
294+ assert source_data ["data" ]["source" ]["source_url_display" ] == "https://x.com/i/web/status/1234567890123456789"
295+
296+
297+ def test_source_map_kept_after_clear_history (client ):
298+ res = client .post ("/api/download/jobs" , json = {"url" : "https://example.com/keep-source" })
299+ job_id = res .get_json ()["data" ]["job" ]["id" ]
300+ _wait_for_job (client , job_id )
301+
302+ clear_res = client .post ("/api/download/jobs/clear" )
303+ assert clear_res .status_code == 200
304+
305+ source_res = client .get ("/api/source" , query_string = {"file" : "mock-output.mp4" })
306+ source_data = source_res .get_json ()
307+ assert source_res .status_code == 200
308+ assert source_data ["success" ] is True
309+ assert source_data ["data" ]["source" ]["source_url_raw" ] == "https://example.com/keep-source"
310+
311+
312+ def test_source_resolve_from_info_json (client ):
313+ media_root = client .application .config ["MEDIA_ROOT" ]
314+ media_file = media_root / "fallback-info.mp4"
315+ media_file .write_bytes (b"00" )
316+ info_file = media_root / "fallback-info.info.json"
317+ info_file .write_text (
318+ '{"webpage_url":"https://www.youtube.com/watch?v=abc123&utm_source=mail"}' ,
319+ encoding = "utf-8" ,
320+ )
321+
322+ source_res = client .get ("/api/source" , query_string = {"file" : "fallback-info.mp4" })
323+ source_data = source_res .get_json ()
324+ assert source_res .status_code == 200
325+ assert source_data ["success" ] is True
326+ assert source_data ["data" ]["source" ]["resolved_by" ] == "infojson"
327+ assert source_data ["data" ]["source" ]["source_url_display" ] == "https://www.youtube.com/watch?v=abc123"
328+ assert source_data ["data" ]["source" ]["source_domain" ] == "www.youtube.com"
329+
330+
331+ def test_source_resolve_from_filename (client ):
332+ media_root = client .application .config ["MEDIA_ROOT" ]
333+ name = "twitter__alice__189111222333444555__189111222333444555__20260221__01.mp4"
334+ (media_root / name ).write_bytes (b"00" )
335+
336+ source_res = client .get ("/api/source" , query_string = {"file" : name })
337+ source_data = source_res .get_json ()
338+ assert source_res .status_code == 200
339+ assert source_data ["success" ] is True
340+ assert source_data ["data" ]["source" ]["resolved_by" ] == "filename"
341+ assert source_data ["data" ]["source" ]["source_url_display" ] == "https://x.com/alice/status/189111222333444555"
342+ assert source_data ["data" ]["source" ]["source_domain" ] == "x.com"
343+
344+
345+ def test_source_batch_api (client ):
346+ media_root = client .application .config ["MEDIA_ROOT" ]
347+ media_file = media_root / "batch-fallback.mp4"
348+ media_file .write_bytes (b"00" )
349+ (media_root / "batch-fallback.info.json" ).write_text (
350+ '{"webpage_url":"https://www.tiktok.com/@u/video/12345"}' ,
351+ encoding = "utf-8" ,
352+ )
353+
354+ res = client .post ("/api/source/batch" , json = {"files" : ["batch-fallback.mp4" , "missing.mp4" ]})
355+ data = res .get_json ()
356+ assert res .status_code == 200
357+ assert data ["success" ] is True
358+ assert data ["data" ]["items" ]["batch-fallback.mp4" ]["source_domain" ] == "www.tiktok.com"
359+ assert data ["data" ]["items" ]["missing.mp4" ] is None
360+
361+
362+ def test_delete_file_also_deletes_source_map (client ):
363+ media_root = client .application .config ["MEDIA_ROOT" ]
364+ media_file = media_root / "mock-output.mp4"
365+ media_file .write_bytes (b"00" )
366+
367+ res = client .post ("/api/download/jobs" , json = {"url" : "https://example.com/delete-source" })
368+ job_id = res .get_json ()["data" ]["job" ]["id" ]
369+ _wait_for_job (client , job_id )
370+
371+ delete_res = client .post ("/delete/mock-output.mp4" , follow_redirects = False )
372+ assert delete_res .status_code in {301 , 302 , 303 , 307 , 308 }
373+
374+ source_res = client .get ("/api/source" , query_string = {"file" : "mock-output.mp4" })
375+ source_data = source_res .get_json ()
376+ assert source_res .status_code == 200
377+ assert source_data ["success" ] is True
378+ assert source_data ["data" ]["source" ] is None
0 commit comments