1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
|
"""
Videos-app enrichment (ffprobe/thumbnailing/TMDB, mediacenter.md §5.2/§10)
only ever runs for a group that has a video_root configured, and only for
files under it — see daemon.py's _enrich_new_video_entries. Burning TMDB's
rate limit and the node's CPU on an operator's whole shared index before
they have chosen which folder is actually their media library would be
real, ongoing cost for files never meant to be in the Videos app at all.
Setting or changing the folder (ops.set_app_directory) fires a one-off sweep
(_enrich_video_root_now) of whatever it already contains: the ordinary
per-broadcast path only ever looks at files new since the last broadcast,
so anything already sitting in a folder before it became the video_root
would otherwise never be picked up.
"""
import asyncio
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from meshbay_common.crypto import generate_gek
from meshbay_node import ops
from meshbay_node.config import Config, HubConfig, NodeConfig, GroupConfig, KeystoreConfig
from meshbay_node.daemon import NodeDaemon
from meshbay_node.indexer import DirectoryIndexer
from meshbay_node.indexer.enrich import Enricher
from meshbay_node.media_cache import MediaCache
from meshbay_node.roster import Roster
from conftest import one_root
pytestmark = pytest.mark.asyncio
def _free_port() -> int:
import socket
with socket.socket() as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
async def _make_daemon(tmp_path, shared, group_id):
config = Config(
hub=HubConfig(url="http://localhost:9999", username="testuser"),
node=NodeConfig(quic_port=_free_port(), ui_port=_free_port()),
groups=[GroupConfig(
id=group_id, name="test-group", shared_dir=str(shared),
visibility="private", quic_port=29014,
)],
keystore=KeystoreConfig(path=tmp_path / "keystore.enc"),
data_dir=tmp_path / "data",
)
daemon = NodeDaemon(config)
daemon._broadcast_coalesce_secs = 0.01 # real value would make these tests wait 0.5s
daemon._media_cache = MediaCache(db_path=tmp_path / "media_cache.db")
await daemon._media_cache.open()
daemon._enricher = Enricher(daemon._media_cache)
daemon._roster = Roster(db_path=tmp_path / "roster.db")
await daemon._roster.open()
return daemon
async def _teardown(daemon):
await daemon._media_cache.close()
await daemon._roster.close()
async def test_no_video_root_means_no_enrichment_at_all(tmp_path):
group_id = "a" * 32
shared = tmp_path / "shared"
shared.mkdir()
(shared / "movie.mkv").write_bytes(b"not a real video, just needs to be indexed as one")
daemon = await _make_daemon(tmp_path, shared, group_id)
try:
indexer = DirectoryIndexer(
roots=one_root(shared), group_id=group_id,
sk_node=Ed25519PrivateKey.generate(), gek=generate_gek())
await indexer.initial_scan()
await daemon._on_index_change(indexer)
await asyncio.sleep(0.05)
assert not daemon._enriched_attempted, (
"a group with no video_root configured must not enrich anything, "
"not even fall back to the whole index")
finally:
await _teardown(daemon)
async def test_only_entries_under_the_configured_root_are_enriched(tmp_path):
group_id = "a" * 32
shared = tmp_path / "shared"
shared.mkdir()
(shared / "Movies").mkdir()
# Distinct content: the index dedupes by content hash, and two files with
# the same bytes would otherwise collapse into a single entry.
(shared / "Movies" / "in-root.mkv").write_bytes(b"in-root content")
(shared / "outside.mkv").write_bytes(b"outside content")
daemon = await _make_daemon(tmp_path, shared, group_id)
try:
await daemon._roster.set_app_directories(group_id, "video", ["shared/Movies"], set_by="op")
indexer = DirectoryIndexer(
roots=one_root(shared), group_id=group_id,
sk_node=Ed25519PrivateKey.generate(), gek=generate_gek())
await indexer.initial_scan()
await daemon._on_index_change(indexer)
await asyncio.sleep(0.05)
by_name = {e.name: e for e in indexer.index.entries}
assert (group_id, by_name["in-root.mkv"].id) in daemon._enriched_attempted
assert (group_id, by_name["outside.mkv"].id) not in daemon._enriched_attempted, (
"a file outside the configured video_root must never be enriched")
finally:
await _teardown(daemon)
async def test_setting_the_video_root_sweeps_what_it_already_contains(tmp_path):
group_id = "a" * 32
shared = tmp_path / "shared"
shared.mkdir()
(shared / "Movies").mkdir()
(shared / "Movies" / "already-there.mkv").write_bytes(b"x")
daemon = await _make_daemon(tmp_path, shared, group_id)
try:
indexer = DirectoryIndexer(
roots=one_root(shared), group_id=group_id,
sk_node=Ed25519PrivateKey.generate(), gek=generate_gek())
await indexer.initial_scan()
daemon._state["indexers"][group_id] = indexer
# Broadcast once with nothing configured — nothing should be scheduled.
await daemon._on_index_change(indexer)
await asyncio.sleep(0.05)
assert not daemon._enriched_attempted
# Now the operator points video_root at the folder that already held
# this file all along.
state = {
"roster": daemon._roster,
# Real roots, because ops now refuses a directory that is not
# inside one — the per-app setters this replaced validated nothing.
"groups_ctx": {group_id: {"roots": one_root(shared)}},
"enrich_app_dirs_fns": {"video": daemon._enrich_video_root_now},
}
await ops.set_app_directory(state, group_id, "video",
"shared/Movies")
await asyncio.sleep(0.05) # let the fire-and-forget sweep actually run
entry = next(iter(indexer.index.entries))
assert (group_id, entry.id) in daemon._enriched_attempted, (
"a file already sitting in the newly-chosen root must be picked "
"up by the sweep, not wait for some unrelated future change")
finally:
await _teardown(daemon)
|