mirror of
https://github.com/alexta69/metube.git
synced 2026-09-21 13:35:01 +00:00
feat: let a subscription carry clip bounds (closes #1049)
A subscription already carries every other download option and applies it to
each video it queues. Clip bounds were the one exception: 4f83174 added them
for one-off downloads and carved subscriptions out, rejecting the fields in the
subscribe route and stripping them in the UI before the request was built. So
the fields sat visible in the shared advanced-options panel while quietly doing
nothing for a subscription.
Carry them like the rest: two fields on SubscriptionInfo, threaded through the
check flow to add_entry. Stored records that predate the fields take the
defaults, since _from_stored filters by field name.
One thing does not carry over. parse_download_options reads a YouTube t=
timestamp from the URL and turns it into a clip start, which is right when you
paste a link to a moment in a video you want. A subscription URL is a channel
or a playlist, so a timestamp left on it says nothing about the videos that
feed will yield, and honouring it would silently truncate every future
download. The subscribe route therefore takes clip bounds only when the caller
sent the fields explicitly; the t= param is still stripped from the stored URL.
Worth knowing when using this: the range is a fixed offset applied to every
video, so it suits feeds with a consistent shape - a standing intro, a fixed
sponsor read - and will cut in the wrong place on a feed without one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+57
-4
@@ -349,17 +349,70 @@ async def test_add_passes_clip_bounds_to_queue(mock_dqueue):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe_rejects_clip_options(mock_dqueue, monkeypatch):
|
||||
monkeypatch.setattr(main.submgr, "add_subscription", AsyncMock())
|
||||
async def test_subscribe_passes_clip_bounds(mock_dqueue, monkeypatch):
|
||||
"""Issue #1049: a subscription's options apply to every future download, and
|
||||
clip bounds were the one option carved out of that."""
|
||||
monkeypatch.setattr(main.submgr, "add_subscription", AsyncMock(return_value={"status": "ok"}))
|
||||
req = _json_request(
|
||||
{
|
||||
**_valid_video_add_body(clip_start="10"),
|
||||
**_valid_video_add_body(clip_start="2:26", clip_end="3:24"),
|
||||
"check_interval_minutes": 60,
|
||||
}
|
||||
)
|
||||
resp = await main.subscribe(req)
|
||||
assert resp.status == 200
|
||||
kwargs = main.submgr.add_subscription.await_args.kwargs
|
||||
assert kwargs["clip_start"] == pytest.approx(146.0)
|
||||
assert kwargs["clip_end"] == pytest.approx(204.0)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe_without_clip_fields_stores_none(mock_dqueue, monkeypatch):
|
||||
monkeypatch.setattr(main.submgr, "add_subscription", AsyncMock(return_value={"status": "ok"}))
|
||||
req = _json_request({**_valid_video_add_body(), "check_interval_minutes": 60})
|
||||
await main.subscribe(req)
|
||||
kwargs = main.submgr.add_subscription.await_args.kwargs
|
||||
assert kwargs["clip_start"] is None
|
||||
assert kwargs["clip_end"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe_ignores_t_param_in_url(mock_dqueue, monkeypatch):
|
||||
"""A t= timestamp means "start here" for a one-off download of that video.
|
||||
On a channel or playlist URL it says nothing about the videos it yields, so
|
||||
it must not silently clip every future download."""
|
||||
monkeypatch.setattr(main.submgr, "add_subscription", AsyncMock(return_value={"status": "ok"}))
|
||||
body = _valid_video_add_body()
|
||||
# t= is only honoured on YouTube hosts, so this must be one to exercise it.
|
||||
body["url"] = "https://www.youtube.com/@somechannel?t=90"
|
||||
req = _json_request({**body, "check_interval_minutes": 60})
|
||||
await main.subscribe(req)
|
||||
kwargs = main.submgr.add_subscription.await_args.kwargs
|
||||
assert kwargs["clip_start"] is None
|
||||
assert kwargs["clip_end"] is None
|
||||
# The timestamp is still stripped from the URL that gets stored.
|
||||
assert "t=90" not in main.submgr.add_subscription.await_args.args[0]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe_explicit_clip_wins_over_t_param(mock_dqueue, monkeypatch):
|
||||
monkeypatch.setattr(main.submgr, "add_subscription", AsyncMock(return_value={"status": "ok"}))
|
||||
body = _valid_video_add_body(clip_start="30")
|
||||
body["url"] = "https://www.youtube.com/@somechannel?t=90"
|
||||
req = _json_request({**body, "check_interval_minutes": 60})
|
||||
await main.subscribe(req)
|
||||
kwargs = main.submgr.add_subscription.await_args.kwargs
|
||||
assert kwargs["clip_start"] == pytest.approx(30.0)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscribe_still_rejects_clips_for_non_media(mock_dqueue, monkeypatch):
|
||||
monkeypatch.setattr(main.submgr, "add_subscription", AsyncMock(return_value={"status": "ok"}))
|
||||
body = _valid_video_add_body(clip_start="10")
|
||||
body["download_type"] = "thumbnail"
|
||||
req = _json_request({**body, "check_interval_minutes": 60})
|
||||
with pytest.raises(web.HTTPBadRequest):
|
||||
await main.subscribe(req)
|
||||
main.submgr.add_subscription.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user