mirror of
https://github.com/alexta69/metube.git
synced 2026-09-21 21:45:04 +00:00
feat: carry the SponsorBlock toggle into subscriptions
Subscriptions download unattended, which is where skipping sponsor reads is most useful, so the flag now travels the same path the other download options take: stored on SubscriptionInfo, persisted in the record, and passed to add_entry for every entry a check queues. Like the clip bounds, it is set when the subscription is created; the update endpoint's field list is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -973,6 +973,7 @@ async def subscribe(request):
|
|||||||
subtitle_mode=o['subtitle_mode'],
|
subtitle_mode=o['subtitle_mode'],
|
||||||
ytdl_options_presets=o['ytdl_options_presets'],
|
ytdl_options_presets=o['ytdl_options_presets'],
|
||||||
ytdl_options_overrides=o['ytdl_options_overrides'],
|
ytdl_options_overrides=o['ytdl_options_overrides'],
|
||||||
|
sponsorblock=o['sponsorblock'],
|
||||||
title_regex=post.get('title_regex'),
|
title_regex=post.get('title_regex'),
|
||||||
skip_subscriber_only=skip_subscriber_only,
|
skip_subscriber_only=skip_subscriber_only,
|
||||||
clip_start=sub_clip_start,
|
clip_start=sub_clip_start,
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ class SubscriptionInfo:
|
|||||||
auto_start: bool = True
|
auto_start: bool = True
|
||||||
playlist_item_limit: int = 0
|
playlist_item_limit: int = 0
|
||||||
split_by_chapters: bool = False
|
split_by_chapters: bool = False
|
||||||
|
sponsorblock: bool = False
|
||||||
chapter_template: str = ""
|
chapter_template: str = ""
|
||||||
subtitle_language: str = "en"
|
subtitle_language: str = "en"
|
||||||
subtitle_mode: str = "prefer_manual"
|
subtitle_mode: str = "prefer_manual"
|
||||||
@@ -242,6 +243,7 @@ def _subscription_to_record(sub: SubscriptionInfo) -> dict[str, Any]:
|
|||||||
"auto_start": sub.auto_start,
|
"auto_start": sub.auto_start,
|
||||||
"playlist_item_limit": sub.playlist_item_limit,
|
"playlist_item_limit": sub.playlist_item_limit,
|
||||||
"split_by_chapters": sub.split_by_chapters,
|
"split_by_chapters": sub.split_by_chapters,
|
||||||
|
"sponsorblock": sub.sponsorblock,
|
||||||
"chapter_template": sub.chapter_template,
|
"chapter_template": sub.chapter_template,
|
||||||
"subtitle_language": sub.subtitle_language,
|
"subtitle_language": sub.subtitle_language,
|
||||||
"subtitle_mode": sub.subtitle_mode,
|
"subtitle_mode": sub.subtitle_mode,
|
||||||
@@ -487,6 +489,7 @@ class SubscriptionManager:
|
|||||||
ytdl_options_overrides: Optional[dict[str, Any]] = None,
|
ytdl_options_overrides: Optional[dict[str, Any]] = None,
|
||||||
clip_start: Optional[float] = None,
|
clip_start: Optional[float] = None,
|
||||||
clip_end: Optional[float] = None,
|
clip_end: Optional[float] = None,
|
||||||
|
sponsorblock: bool = False,
|
||||||
) -> tuple[list[str], list[str]]:
|
) -> tuple[list[str], list[str]]:
|
||||||
queued_ids: list[str] = []
|
queued_ids: list[str] = []
|
||||||
queue_errors: list[str] = []
|
queue_errors: list[str] = []
|
||||||
@@ -519,6 +522,7 @@ class SubscriptionManager:
|
|||||||
ytdl_options_overrides,
|
ytdl_options_overrides,
|
||||||
clip_start,
|
clip_start,
|
||||||
clip_end,
|
clip_end,
|
||||||
|
sponsorblock=sponsorblock,
|
||||||
)
|
)
|
||||||
if isinstance(result, dict) and result.get("status") == "error":
|
if isinstance(result, dict) and result.get("status") == "error":
|
||||||
msg = str(result.get("msg") or f"Queueing failed for {vurl}")
|
msg = str(result.get("msg") or f"Queueing failed for {vurl}")
|
||||||
@@ -606,6 +610,7 @@ class SubscriptionManager:
|
|||||||
subtitle_mode: str,
|
subtitle_mode: str,
|
||||||
ytdl_options_presets: Optional[list[str]] = None,
|
ytdl_options_presets: Optional[list[str]] = None,
|
||||||
ytdl_options_overrides: Optional[dict[str, Any]] = None,
|
ytdl_options_overrides: Optional[dict[str, Any]] = None,
|
||||||
|
sponsorblock: bool = False,
|
||||||
title_regex: Any = None,
|
title_regex: Any = None,
|
||||||
skip_subscriber_only: Any = None,
|
skip_subscriber_only: Any = None,
|
||||||
clip_start: Optional[float] = None,
|
clip_start: Optional[float] = None,
|
||||||
@@ -689,6 +694,7 @@ class SubscriptionManager:
|
|||||||
auto_start=bool(auto_start),
|
auto_start=bool(auto_start),
|
||||||
playlist_item_limit=int(playlist_item_limit),
|
playlist_item_limit=int(playlist_item_limit),
|
||||||
split_by_chapters=bool(split_by_chapters),
|
split_by_chapters=bool(split_by_chapters),
|
||||||
|
sponsorblock=bool(sponsorblock),
|
||||||
chapter_template=chapter_template or "",
|
chapter_template=chapter_template or "",
|
||||||
subtitle_language=subtitle_language,
|
subtitle_language=subtitle_language,
|
||||||
subtitle_mode=subtitle_mode,
|
subtitle_mode=subtitle_mode,
|
||||||
@@ -942,6 +948,7 @@ class SubscriptionManager:
|
|||||||
dl_plimit = cur.playlist_item_limit
|
dl_plimit = cur.playlist_item_limit
|
||||||
dl_autostart = cur.auto_start
|
dl_autostart = cur.auto_start
|
||||||
dl_split = cur.split_by_chapters
|
dl_split = cur.split_by_chapters
|
||||||
|
dl_sponsorblock = cur.sponsorblock
|
||||||
dl_chapter = cur.chapter_template
|
dl_chapter = cur.chapter_template
|
||||||
dl_sublang = cur.subtitle_language
|
dl_sublang = cur.subtitle_language
|
||||||
dl_submode = cur.subtitle_mode
|
dl_submode = cur.subtitle_mode
|
||||||
@@ -1010,6 +1017,7 @@ class SubscriptionManager:
|
|||||||
playlist_item_limit=dl_plimit,
|
playlist_item_limit=dl_plimit,
|
||||||
auto_start=dl_autostart,
|
auto_start=dl_autostart,
|
||||||
split_by_chapters=dl_split,
|
split_by_chapters=dl_split,
|
||||||
|
sponsorblock=dl_sponsorblock,
|
||||||
chapter_template=dl_chapter or "",
|
chapter_template=dl_chapter or "",
|
||||||
subtitle_language=dl_sublang,
|
subtitle_language=dl_sublang,
|
||||||
subtitle_mode=dl_submode,
|
subtitle_mode=dl_submode,
|
||||||
|
|||||||
@@ -366,6 +366,25 @@ async def test_subscribe_passes_clip_bounds(mock_dqueue, monkeypatch):
|
|||||||
assert kwargs["clip_end"] == pytest.approx(204.0)
|
assert kwargs["clip_end"] == pytest.approx(204.0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_subscribe_passes_sponsorblock(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, "sponsorblock": True}
|
||||||
|
)
|
||||||
|
resp = await main.subscribe(req)
|
||||||
|
assert resp.status == 200
|
||||||
|
assert main.submgr.add_subscription.await_args.kwargs["sponsorblock"] is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_subscribe_defaults_sponsorblock_off(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)
|
||||||
|
assert main.submgr.add_subscription.await_args.kwargs["sponsorblock"] is False
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_subscribe_without_clip_fields_stores_none(mock_dqueue, monkeypatch):
|
async def test_subscribe_without_clip_fields_stores_none(mock_dqueue, monkeypatch):
|
||||||
monkeypatch.setattr(main.submgr, "add_subscription", AsyncMock(return_value={"status": "ok"}))
|
monkeypatch.setattr(main.submgr, "add_subscription", AsyncMock(return_value={"status": "ok"}))
|
||||||
|
|||||||
@@ -479,6 +479,69 @@ class SubscriptionPersistenceTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertEqual(reloaded.get(sub_id).clip_start, 12.5)
|
self.assertEqual(reloaded.get(sub_id).clip_start, 12.5)
|
||||||
self.assertIsNone(reloaded.get(sub_id).clip_end)
|
self.assertIsNone(reloaded.get(sub_id).clip_end)
|
||||||
|
|
||||||
|
async def test_check_now_applies_subscription_sponsorblock(self):
|
||||||
|
"""Subscriptions download unattended, so the sponsor-segment removal has
|
||||||
|
to reach every entry the subscription queues, not just manual adds."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
queue = _Queue()
|
||||||
|
mgr = SubscriptionManager(_Config(tmp), queue, _Notifier())
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"subscriptions.extract_flat_playlist",
|
||||||
|
side_effect=[
|
||||||
|
(
|
||||||
|
{"_type": "channel", "title": "Channel"},
|
||||||
|
[{"id": "v1", "title": "One", "webpage_url": "https://example.com/v1"}],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"_type": "channel", "title": "Channel"},
|
||||||
|
[
|
||||||
|
{"id": "v2", "title": "Two", "webpage_url": "https://example.com/v2"},
|
||||||
|
{"id": "v1", "title": "One", "webpage_url": "https://example.com/v1"},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
):
|
||||||
|
result = await mgr.add_subscription(
|
||||||
|
"https://example.com/channel",
|
||||||
|
check_interval_minutes=60,
|
||||||
|
download_type="video",
|
||||||
|
codec="auto",
|
||||||
|
format="any",
|
||||||
|
quality="best",
|
||||||
|
folder="",
|
||||||
|
custom_name_prefix="",
|
||||||
|
auto_start=True,
|
||||||
|
playlist_item_limit=0,
|
||||||
|
split_by_chapters=False,
|
||||||
|
chapter_template="",
|
||||||
|
subtitle_language="en",
|
||||||
|
subtitle_mode="prefer_manual",
|
||||||
|
sponsorblock=True,
|
||||||
|
)
|
||||||
|
sub_id = result["subscription"]["id"]
|
||||||
|
self.assertTrue(mgr.get(sub_id).sponsorblock)
|
||||||
|
await mgr.check_now([sub_id])
|
||||||
|
|
||||||
|
self.assertEqual(len(queue.entries), 1)
|
||||||
|
_entry, _args, kwargs = queue.entries[0]
|
||||||
|
self.assertIs(kwargs["sponsorblock"], True)
|
||||||
|
|
||||||
|
async def test_sponsorblock_survives_reload_and_defaults_to_false(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
cfg = _Config(tmp)
|
||||||
|
mgr = SubscriptionManager(cfg, _Queue(), _Notifier())
|
||||||
|
sub_id = await self._add_one_subscription(mgr)
|
||||||
|
# Records written before the field existed simply take the default.
|
||||||
|
self.assertFalse(mgr.get(sub_id).sponsorblock)
|
||||||
|
|
||||||
|
mgr.get(sub_id).sponsorblock = True
|
||||||
|
async with mgr._lock:
|
||||||
|
mgr._save_locked()
|
||||||
|
|
||||||
|
reloaded = SubscriptionManager(cfg, _Queue(), _Notifier())
|
||||||
|
self.assertTrue(reloaded.get(sub_id).sponsorblock)
|
||||||
|
|
||||||
async def test_check_now_queues_subscriber_only_when_skip_disabled(self):
|
async def test_check_now_queues_subscriber_only_when_skip_disabled(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
queue = _Queue()
|
queue = _Queue()
|
||||||
|
|||||||
@@ -2008,6 +2008,7 @@ class DownloadQueue:
|
|||||||
ytdl_options_overrides=None,
|
ytdl_options_overrides=None,
|
||||||
clip_start=None,
|
clip_start=None,
|
||||||
clip_end=None,
|
clip_end=None,
|
||||||
|
sponsorblock=False,
|
||||||
):
|
):
|
||||||
if ytdl_options_presets is None:
|
if ytdl_options_presets is None:
|
||||||
ytdl_options_presets = []
|
ytdl_options_presets = []
|
||||||
@@ -2033,6 +2034,7 @@ class DownloadQueue:
|
|||||||
clip_end,
|
clip_end,
|
||||||
already,
|
already,
|
||||||
None,
|
None,
|
||||||
|
sponsorblock=sponsorblock,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def start_pending(self, ids):
|
async def start_pending(self, ids):
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { SubscriptionsService, SubscribePayload } from './subscriptions.service';
|
||||||
|
import { MeTubeSocket } from './metube-socket.service';
|
||||||
|
|
||||||
|
class MeTubeSocketStub {
|
||||||
|
private subjects: Record<string, Subject<string>> = {};
|
||||||
|
|
||||||
|
fromEvent(event: string) {
|
||||||
|
if (!this.subjects[event]) {
|
||||||
|
this.subjects[event] = new Subject<string>();
|
||||||
|
}
|
||||||
|
return this.subjects[event].asObservable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function basePayload(): SubscribePayload {
|
||||||
|
return {
|
||||||
|
url: 'https://example.com/channel',
|
||||||
|
downloadType: 'video',
|
||||||
|
codec: 'auto',
|
||||||
|
quality: 'best',
|
||||||
|
format: 'any',
|
||||||
|
folder: '',
|
||||||
|
customNamePrefix: '',
|
||||||
|
playlistItemLimit: 0,
|
||||||
|
autoStart: true,
|
||||||
|
splitByChapters: false,
|
||||||
|
sponsorblock: false,
|
||||||
|
chapterTemplate: '',
|
||||||
|
subtitleLanguage: 'en',
|
||||||
|
subtitleMode: 'prefer_manual',
|
||||||
|
ytdlOptionsPresets: [],
|
||||||
|
ytdlOptionsOverrides: '',
|
||||||
|
clipStart: '',
|
||||||
|
clipEnd: '',
|
||||||
|
checkIntervalMinutes: 60,
|
||||||
|
titleRegex: '',
|
||||||
|
skipSubscriberOnly: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SubscriptionsService', () => {
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
let service: SubscriptionsService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
SubscriptionsService,
|
||||||
|
provideHttpClient(),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
{ provide: MeTubeSocket, useValue: new MeTubeSocketStub() },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
service = TestBed.inject(SubscriptionsService);
|
||||||
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribe() carries the sponsorblock flag', () => {
|
||||||
|
service.subscribe({ ...basePayload(), sponsorblock: true }).subscribe();
|
||||||
|
const req = httpMock.expectOne('subscribe');
|
||||||
|
expect(req.request.method).toBe('POST');
|
||||||
|
expect(req.request.body).toEqual(expect.objectContaining({ sponsorblock: true }));
|
||||||
|
req.flush({ status: 'ok' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribe() sends the flag off by default', () => {
|
||||||
|
service.subscribe(basePayload()).subscribe();
|
||||||
|
const req = httpMock.expectOne('subscribe');
|
||||||
|
expect(req.request.body).toEqual(expect.objectContaining({ sponsorblock: false }));
|
||||||
|
req.flush({ status: 'ok' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -92,6 +92,7 @@ export class SubscriptionsService {
|
|||||||
playlist_item_limit: payload.playlistItemLimit,
|
playlist_item_limit: payload.playlistItemLimit,
|
||||||
auto_start: payload.autoStart,
|
auto_start: payload.autoStart,
|
||||||
split_by_chapters: payload.splitByChapters,
|
split_by_chapters: payload.splitByChapters,
|
||||||
|
sponsorblock: payload.sponsorblock,
|
||||||
chapter_template: payload.chapterTemplate,
|
chapter_template: payload.chapterTemplate,
|
||||||
subtitle_language: payload.subtitleLanguage,
|
subtitle_language: payload.subtitleLanguage,
|
||||||
subtitle_mode: payload.subtitleMode,
|
subtitle_mode: payload.subtitleMode,
|
||||||
|
|||||||
Reference in New Issue
Block a user