|
| 1 | +import os |
| 2 | + |
| 3 | +import pytest |
| 4 | + |
| 5 | +from tiklocal.app import create_app |
| 6 | + |
| 7 | + |
| 8 | +@pytest.fixture |
| 9 | +def client(tmp_path, monkeypatch): |
| 10 | + media_root = tmp_path / "media" |
| 11 | + media_root.mkdir(parents=True, exist_ok=True) |
| 12 | + for i in range(6): |
| 13 | + (media_root / f"v{i:02d}.mp4").write_bytes(b"00") |
| 14 | + for i in range(6): |
| 15 | + (media_root / f"i{i:02d}.jpg").write_bytes(b"00") |
| 16 | + |
| 17 | + # Create one large video for big_files mode. |
| 18 | + (media_root / "big.mp4").write_bytes(b"0" * (2 * 1024 * 1024)) |
| 19 | + |
| 20 | + # Keep stable ordering for latest mode. |
| 21 | + for idx, p in enumerate(sorted(media_root.iterdir())): |
| 22 | + ts = 1_700_000_000 + idx |
| 23 | + os.utime(p, (ts, ts)) |
| 24 | + |
| 25 | + data_root = tmp_path / "tiklocal-data" |
| 26 | + monkeypatch.setenv("MEDIA_ROOT", str(media_root)) |
| 27 | + monkeypatch.setenv("TIKLOCAL_INSTANCE", str(data_root)) |
| 28 | + |
| 29 | + app = create_app({"TESTING": True, "MEDIA_ROOT": media_root}) |
| 30 | + return app.test_client() |
| 31 | + |
| 32 | + |
| 33 | +def test_library_page_has_mode_tabs_and_no_masonry_label(client): |
| 34 | + res = client.get("/library") |
| 35 | + assert res.status_code == 200 |
| 36 | + body = res.data.decode("utf-8") |
| 37 | + assert "data-mode=\"all\"" in body |
| 38 | + assert "data-mode=\"image_random\"" in body |
| 39 | + assert "data-mode=\"video_latest\"" in body |
| 40 | + assert "data-mode=\"big_files\"" in body |
| 41 | + assert "Masonry" not in body |
| 42 | + assert "id=\"quick-source\"" in body |
| 43 | + assert "id=\"quick-close-top\"" in body |
| 44 | + assert "id=\"quick-speed\"" in body |
| 45 | + assert "id=\"quick-caption\"" in body |
| 46 | + assert "id=\"quick-magnifier\"" in body |
| 47 | + assert "id=\"quick-play-status\"" in body |
| 48 | + assert "#quick-view.immersive .quick-caption-panel" in body |
| 49 | + assert "image-focus" not in body |
| 50 | + assert "flow_ui_shared.js" in body |
| 51 | + assert "flow_state_controller.js" in body |
| 52 | + assert "createFlowStateController(" in body |
| 53 | + assert "uiShared.updateMagnifierContent({" in body |
| 54 | + |
| 55 | + |
| 56 | +def test_home_feed_uses_unified_immersive_model(client): |
| 57 | + res = client.get("/") |
| 58 | + assert res.status_code == 200 |
| 59 | + body = res.data.decode("utf-8") |
| 60 | + assert "body.immersive-mode .caption-panel:not(.is-hidden)" in body |
| 61 | + assert "flow_ui_shared.js" in body |
| 62 | + assert "flow_state_controller.js" in body |
| 63 | + assert "createFlowStateController(" in body |
| 64 | + assert "flowState.toggleImmersive()" in body |
| 65 | + assert "flowState.setMagnifying(active)" in body |
| 66 | + assert "uiShared.updateMagnifierContent({" in body |
| 67 | + assert "image-focus-mode" not in body |
| 68 | + |
| 69 | + |
| 70 | +def test_api_library_items_supports_modes_and_seed(client): |
| 71 | + all_res = client.get("/api/library/items?scope=all&mode=all&offset=0&limit=20") |
| 72 | + assert all_res.status_code == 200 |
| 73 | + all_data = all_res.get_json() |
| 74 | + assert all_data["success"] is True |
| 75 | + all_items = all_data["data"]["items"] |
| 76 | + assert any(item["type"] == "video" for item in all_items) |
| 77 | + assert any(item["type"] == "image" for item in all_items) |
| 78 | + |
| 79 | + video_res = client.get("/api/library/items?scope=all&mode=video_latest&offset=0&limit=20") |
| 80 | + video_items = video_res.get_json()["data"]["items"] |
| 81 | + assert len(video_items) > 0 |
| 82 | + assert all(item["type"] == "video" for item in video_items) |
| 83 | + |
| 84 | + image_res = client.get("/api/library/items?scope=all&mode=image_random&offset=0&limit=20&seed=fixed") |
| 85 | + image_items = image_res.get_json()["data"]["items"] |
| 86 | + assert len(image_items) > 0 |
| 87 | + assert all(item["type"] == "image" for item in image_items) |
| 88 | + assert image_res.get_json()["data"]["seed"] == "fixed" |
| 89 | + |
| 90 | + big_res = client.get("/api/library/items?scope=all&mode=big_files&offset=0&limit=20&min_mb=1") |
| 91 | + big_items = big_res.get_json()["data"]["items"] |
| 92 | + assert len(big_items) >= 1 |
| 93 | + assert all(item["type"] == "video" for item in big_items) |
| 94 | + assert any(item["name"] == "big.mp4" for item in big_items) |
| 95 | + |
| 96 | + |
| 97 | +def test_api_library_items_no_duplicates_across_offsets(client): |
| 98 | + seen = set() |
| 99 | + offset = 0 |
| 100 | + for _ in range(8): |
| 101 | + res = client.get(f"/api/library/items?scope=all&mode=all&offset={offset}&limit=4") |
| 102 | + assert res.status_code == 200 |
| 103 | + data = res.get_json()["data"] |
| 104 | + names = [item["name"] for item in data["items"]] |
| 105 | + for name in names: |
| 106 | + assert name not in seen |
| 107 | + seen.add(name) |
| 108 | + if not data["has_more"]: |
| 109 | + break |
| 110 | + offset = int(data["next_offset"]) |
| 111 | + |
| 112 | + |
| 113 | +def test_api_library_items_dedupes_symlink_aliases(tmp_path, monkeypatch): |
| 114 | + media_root = tmp_path / "media" |
| 115 | + media_root.mkdir(parents=True, exist_ok=True) |
| 116 | + (media_root / "origin.mp4").write_bytes(b"abc") |
| 117 | + (media_root / "img.jpg").write_bytes(b"abc") |
| 118 | + |
| 119 | + alias = media_root / "alias.mp4" |
| 120 | + try: |
| 121 | + alias.symlink_to(media_root / "origin.mp4") |
| 122 | + except (OSError, NotImplementedError): |
| 123 | + pytest.skip("Symlink not supported in this environment") |
| 124 | + |
| 125 | + data_root = tmp_path / "tiklocal-data" |
| 126 | + monkeypatch.setenv("MEDIA_ROOT", str(media_root)) |
| 127 | + monkeypatch.setenv("TIKLOCAL_INSTANCE", str(data_root)) |
| 128 | + app = create_app({"TESTING": True, "MEDIA_ROOT": media_root}) |
| 129 | + local_client = app.test_client() |
| 130 | + |
| 131 | + res = local_client.get("/api/library/items?scope=all&mode=all&offset=0&limit=50") |
| 132 | + assert res.status_code == 200 |
| 133 | + items = res.get_json()["data"]["items"] |
| 134 | + video_items = [item for item in items if item["type"] == "video"] |
| 135 | + assert len(video_items) == 1 |
| 136 | + assert video_items[0]["name"] in {"origin.mp4", "alias.mp4"} |
| 137 | + |
| 138 | + |
| 139 | +def test_legacy_browse_and_gallery_redirects(client): |
| 140 | + browse = client.get("/browse", follow_redirects=False) |
| 141 | + assert browse.status_code in {301, 302, 308} |
| 142 | + assert browse.headers.get("Location", "").startswith("/library?mode=video_latest") |
| 143 | + |
| 144 | + browse_big = client.get("/browse?filter=big&min_mb=3", follow_redirects=False) |
| 145 | + assert browse_big.status_code in {301, 302, 308} |
| 146 | + assert "mode=big_files" in browse_big.headers.get("Location", "") |
| 147 | + assert "min_mb=3" in browse_big.headers.get("Location", "") |
| 148 | + |
| 149 | + gallery = client.get("/gallery", follow_redirects=False) |
| 150 | + assert gallery.status_code in {301, 302, 308} |
| 151 | + assert gallery.headers.get("Location", "").startswith("/library?mode=image_random") |
| 152 | + |
| 153 | + |
| 154 | +def test_favorite_scope_and_detail_links(client): |
| 155 | + client.post("/api/favorite/v01.mp4") |
| 156 | + client.post("/api/favorite/i01.jpg") |
| 157 | + |
| 158 | + res = client.get("/api/library/items?scope=favorite&mode=all&offset=0&limit=20") |
| 159 | + assert res.status_code == 200 |
| 160 | + data = res.get_json()["data"] |
| 161 | + names = {item["name"] for item in data["items"]} |
| 162 | + assert "v01.mp4" in names |
| 163 | + assert "i01.jpg" in names |
| 164 | + assert any(item["detail_url"] == "/detail/v01.mp4" for item in data["items"]) |
| 165 | + assert any(item["detail_url"] == "/image?uri=i01.jpg" for item in data["items"]) |
0 commit comments