fix: share, upload, crop, auto-skip, tag chips and See all hold what they claim
- Share: a link that expired is never copied (the dialog and one-click Share pick a live one, else make one), says it expired, and the copy-on-open claim clears on the next copy. - A password-protected file link asks to open a file, not to watch a video. - Upload: Cancel, the close button or the backdrop while naming a folder makes no folder; a failed refetch keeps the target; a folder being made says so. - Crop: an infinite number is dropped, not a 500; an unprobed clip's size follows a shown-size crop; the edit_spec docs name frame_w/frame_h; the join and rotated paths are tested. - Auto-skip: the wait gives way on a marker too short for it, so the skip still fires. - Tag chips count the followed channels (subscribed_count) and a toggled chip keeps the focus. - Continue watching's See all sorts by last watched when in progress was already on.
This commit is contained in:
@@ -20,8 +20,10 @@ from app.downloads.models import DownloadJob, MediaAsset
|
||||
pytestmark = pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not available")
|
||||
|
||||
|
||||
def _source(path: Path, size: str, box: str, sar: int) -> None:
|
||||
"""Red, with the picture's middle half green: a crop of that middle holds no red."""
|
||||
def _source(path: Path, size: str, box: str, sar: int, rotate: int = 0) -> None:
|
||||
"""Red, with a green box: a crop of the box holds no red. `rotate` tags the file to be shown
|
||||
turned by that many degrees counter-clockwise, as a phone records a portrait video."""
|
||||
tag = ["-display_rotation:v:0", str(rotate)] if rotate else []
|
||||
subprocess.run(
|
||||
[
|
||||
"ffmpeg", "-y", "-loglevel", "error",
|
||||
@@ -32,6 +34,13 @@ def _source(path: Path, size: str, box: str, sar: int) -> None:
|
||||
],
|
||||
check=True,
|
||||
) # fmt: skip
|
||||
if tag:
|
||||
tagged = path.with_name("tagged-" + path.name)
|
||||
subprocess.run(
|
||||
["ffmpeg", "-y", "-loglevel", "error", *tag, "-i", str(path), "-c", "copy", str(tagged)],
|
||||
check=True,
|
||||
) # fmt: skip
|
||||
tagged.replace(path)
|
||||
|
||||
|
||||
def _red_and_green(path: Path) -> tuple[int, int]:
|
||||
@@ -50,13 +59,14 @@ def _red_and_green(path: Path) -> tuple[int, int]:
|
||||
return red, green
|
||||
|
||||
|
||||
def _cut(db, make_user, tmp_path, src: Path, crop: dict) -> Path:
|
||||
def _cut_asset(db, make_user, src: Path, spec: dict, then=lambda: None) -> MediaAsset:
|
||||
user = make_user()
|
||||
source = uploadmod.ingest_upload(
|
||||
db, user.id, filename="clip.mp4", src_path=src, size=src.stat().st_size
|
||||
)
|
||||
assert source.status == "done"
|
||||
job = service.enqueue_edit(db, user.id, source, {"crop": crop})
|
||||
then()
|
||||
job = service.enqueue_edit(db, user.id, source, spec)
|
||||
asset = db.get(MediaAsset, job.asset_id)
|
||||
job.status, asset.status = "running", "downloading" # the worker's claim
|
||||
db.commit()
|
||||
@@ -66,6 +76,11 @@ def _cut(db, make_user, tmp_path, src: Path, crop: dict) -> Path:
|
||||
db.expire_all()
|
||||
job, asset = db.get(DownloadJob, job.id), db.get(MediaAsset, job.asset_id)
|
||||
assert (job.status, job.error, asset.status) == ("done", None, "ready")
|
||||
return asset
|
||||
|
||||
|
||||
def _cut(db, make_user, tmp_path, src: Path, crop: dict, **spec) -> Path:
|
||||
asset = _cut_asset(db, make_user, src, {"crop": crop, **spec})
|
||||
return storage.abs_path(str(tmp_path), asset.rel_path)
|
||||
|
||||
|
||||
@@ -110,3 +125,64 @@ def test_a_crop_stored_before_it_carried_its_frame_is_cut_as_it_always_was():
|
||||
def test_a_frame_that_cannot_be_read_is_not_divided_by(frame):
|
||||
spec = edit.normalize_edit_spec({"crop": {"x": 4, "y": 6, "w": 100, "h": 50, **frame}})
|
||||
assert spec["crop"] == {"x": 4, "y": 6, "w": 100, "h": 50}
|
||||
|
||||
|
||||
def test_a_crop_of_a_join_keeps_what_the_frame_showed(db, make_user, tmp_path, monkeypatch):
|
||||
"""A cut-list joins through `filter_complex`, a crop path of its own."""
|
||||
monkeypatch.setattr(settings, "files_root", str(tmp_path))
|
||||
src = tmp_path / "in.mp4"
|
||||
_source(src, "320x240", "x=80:y=60:w=160:h=120", sar=2)
|
||||
|
||||
final = _cut(
|
||||
db, make_user, tmp_path, src,
|
||||
{"x": 160, "y": 60, "w": 320, "h": 120, "frame_w": 640, "frame_h": 240},
|
||||
segments=[{"start_s": 0, "end_s": 0.5}, {"start_s": 1, "end_s": 1.5}], accurate=True,
|
||||
) # fmt: skip
|
||||
|
||||
assert _red_and_green(final) == (0, 160 * 120)
|
||||
|
||||
|
||||
def test_a_crop_of_a_rotated_video_is_cut_where_it_was_drawn(db, make_user, tmp_path, monkeypatch):
|
||||
"""The browser shows a rotated file upright, so the frame was drawn on the upright picture."""
|
||||
monkeypatch.setattr(settings, "files_root", str(tmp_path))
|
||||
src = tmp_path / "in.mp4"
|
||||
# Stored 320 x 240, green on its left half; shown turned a quarter, 240 x 320.
|
||||
_source(src, "320x240", "x=0:y=0:w=160:h=240", sar=1, rotate=90)
|
||||
|
||||
# Turned counter-clockwise, the stored left half is the shown bottom half.
|
||||
final = _cut(
|
||||
db, make_user, tmp_path, src,
|
||||
{"x": 0, "y": 160, "w": 240, "h": 160, "frame_w": 240, "frame_h": 320},
|
||||
) # fmt: skip
|
||||
|
||||
assert _red_and_green(final) == (0, 240 * 160)
|
||||
|
||||
|
||||
def test_a_clip_ffprobe_cannot_size_gets_the_size_its_crop_cut(
|
||||
db, make_user, tmp_path, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr(settings, "files_root", str(tmp_path))
|
||||
src = tmp_path / "in.mp4"
|
||||
_source(src, "320x240", "x=80:y=60:w=160:h=120", sar=2)
|
||||
|
||||
def probe_fails():
|
||||
monkeypatch.setattr(pipeline.probe, "probe_media", lambda _p: (None,) * 5)
|
||||
|
||||
asset = _cut_asset(
|
||||
db, make_user, src,
|
||||
{"crop": {"x": 160, "y": 60, "w": 320, "h": 120, "frame_w": 640, "frame_h": 240}},
|
||||
then=probe_fails,
|
||||
) # fmt: skip
|
||||
|
||||
# Half the shown width of a 320-wide file: 160 stored, not the 320 the frame counted.
|
||||
assert (asset.width, asset.height) == (160, 120)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ["frame_w", "x"])
|
||||
def test_an_infinite_number_in_a_crop_is_dropped_not_a_crash(key):
|
||||
crop = {"x": 4, "y": 6, "w": 100, "h": 50, "frame_w": 640, "frame_h": 240}
|
||||
spec = edit.normalize_edit_spec({"crop": {**crop, key: float("inf")}})
|
||||
if key == "frame_w":
|
||||
assert spec["crop"] == {"x": 4, "y": 6, "w": 100, "h": 50}
|
||||
else:
|
||||
assert "crop" not in spec
|
||||
|
||||
@@ -150,6 +150,7 @@ def test_a_password_folder_link_shows_nothing_until_unlocked(shelf, client):
|
||||
assert client.get(f"/api/public/watch/{token}").json() == {
|
||||
"needs_password": True,
|
||||
"kind": "folder",
|
||||
"video": False,
|
||||
}
|
||||
bare = f"/api/public/watch/{token}/files/{shelf['inside'][0]}/file"
|
||||
assert client.get(bare).status_code == 403
|
||||
@@ -312,7 +313,7 @@ def test_a_password_file_link_says_it_opens_a_file(shelf, client):
|
||||
.json()
|
||||
)
|
||||
meta = client.get(f"/api/public/watch/{made['token']}").json()
|
||||
assert meta == {"needs_password": True, "kind": "file"}
|
||||
assert meta == {"needs_password": True, "kind": "file", "video": True}
|
||||
|
||||
|
||||
def test_a_file_whose_bytes_are_gone_is_not_offered(shelf, client, db):
|
||||
|
||||
@@ -14,7 +14,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.discover import pins
|
||||
from app.catalog.models import Channel, Video
|
||||
from app.catalog.models import Channel, Subscription, Video
|
||||
from app.reader import queries, service
|
||||
from app.reader.models import ChannelTag, ComfortPin, Playlist, PlaylistItem, Tag, VideoState
|
||||
|
||||
@@ -60,12 +60,30 @@ def test_a_system_tag_is_nobodys_to_edit(db, make_user):
|
||||
service.create_tag(db, other, "Theirs", "other", None) # another user's tag stays theirs
|
||||
db.commit()
|
||||
assert queries.own_tag(db, user, system.id) is None
|
||||
assert [t.id for t, _ in queries.tags_with_counts(db, user)] == [system.id]
|
||||
assert [t.id for t, *_ in queries.tags_with_counts(db, user)] == [system.id]
|
||||
|
||||
|
||||
# --- tags ------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_a_tag_counts_apart_the_links_on_channels_the_user_still_follows(db, make_user):
|
||||
"""An unsubscribe keeps the channel's tag links; the Channels page's filter lists only followed
|
||||
channels, so its count must not hold the others."""
|
||||
user, other = make_user(), make_user()
|
||||
tag = service.create_tag(db, user, "Music", "other", None)
|
||||
for cid in ("UCa", "UCb", "UCgone"):
|
||||
db.add(Channel(id=cid, title=cid))
|
||||
db.flush()
|
||||
for cid in ("UCa", "UCb"):
|
||||
db.add(Subscription(user_id=user.id, channel_id=cid))
|
||||
db.add(Subscription(user_id=other.id, channel_id="UCgone"))
|
||||
for cid in ("UCa", "UCb", "UCgone"):
|
||||
db.add(ChannelTag(channel_id=cid, tag_id=tag.id, user_id=user.id, source="user"))
|
||||
db.commit()
|
||||
[(_, count, subscribed)] = [r for r in queries.tags_with_counts(db, user) if r[0].id == tag.id]
|
||||
assert (count, subscribed) == (3, 2)
|
||||
|
||||
|
||||
def test_a_tag_is_made_once_per_name_and_an_unknown_category_is_other(db, make_user):
|
||||
user = make_user()
|
||||
tag = service.create_tag(db, user, "Later", "nonsense", "#fff")
|
||||
|
||||
@@ -135,3 +135,14 @@ def test_a_card_of_a_video_stays_a_video(shared, db):
|
||||
assert f"/watch/{made['token']}" in tags and "video.other" in tags
|
||||
locked = shared("other.mp4", _VIDEO, password="pw")
|
||||
assert "Password-protected video" in og._watch_tags(locked["token"], db)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "data", "video"),
|
||||
[("clip.mp4", _VIDEO, True), ("song.m4a", _AUDIO, False), ("backup.tar.gz", None, False)],
|
||||
)
|
||||
def test_a_password_link_says_whether_it_opens_a_video(shared, client, name, data, video):
|
||||
"""The lock screen asks to watch a video and to open anything else, whichever path it is on."""
|
||||
token = shared(name, data, password="pw")["token"]
|
||||
meta = client.get(f"/api/public/watch/{token}").json()
|
||||
assert meta == {"needs_password": True, "kind": "file", "video": video}
|
||||
|
||||
Reference in New Issue
Block a user