diff --git a/.gitignore b/.gitignore index fb3ff9e..5404eaa 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ htmlcov/ .idea/ .vscode/ .DS_Store -demo/ # Docs build artefacts and generated content site/ diff --git a/demo.gif b/demo.gif deleted file mode 100644 index 312fa9c..0000000 Binary files a/demo.gif and /dev/null differ diff --git a/docs/livestreams.md b/docs/livestreams.md index f60405b..098bb38 100644 --- a/docs/livestreams.md +++ b/docs/livestreams.md @@ -85,12 +85,3 @@ ytstudio livestreams update --latency low --auto-start --execute `update` does not expose the made-for-kids flag, because `liveBroadcasts.update` only accepts `privacyStatus` under `status`. Set the COPPA flag at schedule time. - -## Demo mode - -For a quick offline tour without touching the API: - -```bash -YTSTUDIO_DEMO=1 ytstudio livestreams list --status all -YTSTUDIO_DEMO=1 ytstudio livestreams show demo-broadcast-upcoming-1 --ingest -``` diff --git a/docs/videos.md b/docs/videos.md index f6f5f16..e6d4397 100644 --- a/docs/videos.md +++ b/docs/videos.md @@ -125,14 +125,3 @@ budget you can upload ~6 videos per day. Use `--max` to cap a run explicitly. If the API returns `quotaExceeded` mid-run, the pipeline stops cleanly, prints how many videos succeeded, and exits non-zero. See [API quota](api-quota.md). - -## Demo mode - -Run any video command without authenticating by setting `YTSTUDIO_DEMO=1`: - -```bash -YTSTUDIO_DEMO=1 ytstudio videos list -``` - -Demo mode uses bundled fixtures and never reaches the real API. The upload -command is the one exception: it always talks to YouTube. diff --git a/src/ytstudio/demo.py b/src/ytstudio/demo.py deleted file mode 100644 index 3e221d5..0000000 --- a/src/ytstudio/demo.py +++ /dev/null @@ -1,269 +0,0 @@ -import functools -import json -import os -import time -from datetime import UTC, datetime, timedelta -from pathlib import Path -from typing import ClassVar - -DEMO_MODE = os.environ.get("YTSTUDIO_DEMO", "").lower() in ("1", "true", "yes") - -_DATA_DIR = Path(__file__).parent / "demo_data" - - -@functools.cache -def _load(name: str) -> dict: - return json.loads((_DATA_DIR / name).read_text()) - - -class DemoRequest: - def __init__(self, response: dict, delay: float = 0): - self._response = response - self._delay = delay - - def execute(self) -> dict: - if self._delay: - time.sleep(self._delay) - return self._response - - -class _DemoChannels: - def list(self, **kwargs): - return DemoRequest(_load("channel.json")) - - -class _DemoVideos: - def list(self, **kwargs): - id_param = kwargs.get("id", "") - requested_ids = [i.strip() for i in id_param.split(",") if i.strip()] - - if not requested_ids: - return DemoRequest(_load("videos.json")) - - matched = [v for v in _load("videos.json")["items"] if v["id"] in requested_ids] - return DemoRequest({"items": matched}) - - def update(self, **kwargs): - body = kwargs.get("body", {}) - return DemoRequest(body, delay=0.3) - - -class _DemoPlaylistItems: - def list(self, **kwargs): - max_results = kwargs.get("maxResults", 50) - data = _load("playlist_items.json") - items = data["items"][:max_results] - return DemoRequest( - { - "items": items, - "pageInfo": {"totalResults": len(data["items"])}, - } - ) - - -class _DemoCommentThreads: - def list(self, **kwargs): - max_results = kwargs.get("maxResults", 100) - video_id = kwargs.get("videoId") - - items = _load("comments.json")["items"] - if video_id: - items = [ - c - for c in items - if c["snippet"]["topLevelComment"]["snippet"].get("videoId") == video_id - ] - - return DemoRequest({"items": items[:max_results]}) - - -class _DemoComments: - def setModerationStatus(self, **kwargs): - return DemoRequest({}) - - -class _DemoLiveBroadcasts: - _STATUS_MAP: ClassVar[dict[str, str]] = { - "ready": "upcoming", - "created": "upcoming", - "live": "active", - "testing": "active", - "complete": "completed", - } - - def _all(self) -> list[dict]: - return _load("broadcasts.json")["items"] - - def list(self, **kwargs): - id_param = kwargs.get("id") - status_param = kwargs.get("broadcastStatus", "all") - max_results = kwargs.get("maxResults", 50) - - items = self._all() - if id_param: - ids = {i.strip() for i in str(id_param).split(",") if i.strip()} - items = [b for b in items if b.get("id") in ids] - elif status_param and status_param != "all": - items = [ - b - for b in items - if self._STATUS_MAP.get(b.get("status", {}).get("lifeCycleStatus", "")) - == status_param - ] - return DemoRequest({"items": items[:max_results]}) - - def transition(self, **kwargs): - broadcast_id = kwargs.get("id", "") - target = kwargs.get("broadcastStatus", "live") - return DemoRequest( - { - "id": broadcast_id, - "snippet": {"title": f"Demo broadcast {broadcast_id}"}, - "status": {"lifeCycleStatus": target}, - } - ) - - def insert(self, **kwargs): - body = kwargs.get("body", {}) or {} - snippet = body.get("snippet", {}) - status = body.get("status", {}) - return DemoRequest( - { - "id": "demo-broadcast-new", - "snippet": snippet, - "status": { - "lifeCycleStatus": "created", - "privacyStatus": status.get("privacyStatus", "public"), - "selfDeclaredMadeForKids": status.get("selfDeclaredMadeForKids", False), - }, - "contentDetails": {}, - } - ) - - def update(self, **kwargs): - body = kwargs.get("body", {}) or {} - return DemoRequest(body) - - -class _DemoLiveStreams: - def list(self, **kwargs): - id_param = kwargs.get("id", "") - items = _load("live_streams.json")["items"] - if id_param: - ids = {i.strip() for i in str(id_param).split(",") if i.strip()} - items = [s for s in items if s.get("id") in ids] - return DemoRequest({"items": items}) - - -class DemoDataService: - def channels(self): - return _DemoChannels() - - def videos(self): - return _DemoVideos() - - def playlistItems(self): - return _DemoPlaylistItems() - - def comments(self): - return _DemoComments() - - def commentThreads(self): - return _DemoCommentThreads() - - def liveBroadcasts(self): - return _DemoLiveBroadcasts() - - def liveStreams(self): - return _DemoLiveStreams() - - -class _DemoReports: - def query(self, **params): - metrics = [m.strip() for m in params.get("metrics", "").split(",") if m.strip()] - dimensions = [d.strip() for d in params.get("dimensions", "").split(",") if d.strip()] - filters = params.get("filters", "") - sort = params.get("sort", "") - max_results = params.get("maxResults") - - analytics = _load("analytics_metrics.json") - video_metrics = {k: v for k, v in analytics.items() if k != "countries"} - countries = analytics.get("countries", ["US", "IN", "GB", "DE", "BR"]) - - headers = _make_column_headers(dimensions, metrics) - - def _metric_vals(base: dict) -> list: - return [base.get(m, 0) for m in metrics] - - rows = [] - - # Single video filter - filter_video_id = None - if filters: - for part in filters.split(";"): - if part.startswith("video=="): - filter_video_id = part.split("==", 1)[1] - - if not dimensions: - totals = {m: 0 for m in metrics} - sources = video_metrics - if filter_video_id and filter_video_id in video_metrics: - sources = {filter_video_id: video_metrics[filter_video_id]} - for base in sources.values(): - for m in metrics: - totals[m] += base.get(m, 0) - rows.append([totals[m] for m in metrics]) - - elif dimensions == ["day"]: - today = datetime.now(UTC).date() - n_days = 7 - for i in range(n_days, 0, -1): - date_str = (today - timedelta(days=i)).strftime("%Y-%m-%d") - vid = list(video_metrics.values())[i % len(video_metrics)] - rows.append([date_str, *_metric_vals(vid)]) - - elif dimensions == ["country"]: - for j, country in enumerate(countries): - vid = list(video_metrics.values())[j % len(video_metrics)] - rows.append([country, *_metric_vals(vid)]) - - elif "video" in dimensions: - for vid_id, base in video_metrics.items(): - rows.append([vid_id, *_metric_vals(base)]) - - else: - totals = {m: 0 for m in metrics} - for base in video_metrics.values(): - for m in metrics: - totals[m] += base.get(m, 0) - dim_vals = ["unknown"] * len(dimensions) - rows.append([*dim_vals, *[totals[m] for m in metrics]]) - - # Apply sort - if sort and rows and dimensions: - sort_desc = sort.startswith("-") - sort_field = sort.lstrip("-") - all_names = dimensions + metrics - if sort_field in all_names: - idx = all_names.index(sort_field) - rows.sort(key=lambda r: r[idx], reverse=sort_desc) - - if max_results and len(rows) > max_results: - rows = rows[:max_results] - - return DemoRequest({"columnHeaders": headers, "rows": rows}) - - -def _make_column_headers(dimensions: list[str], metrics: list[str]) -> list[dict]: - return [{"name": d, "columnType": "DIMENSION", "dataType": "STRING"} for d in dimensions] + [ - {"name": m, "columnType": "METRIC", "dataType": "INTEGER"} for m in metrics - ] - - -class DemoAnalyticsService: - def reports(self): - return _DemoReports() - - -def is_demo_mode() -> bool: - return DEMO_MODE diff --git a/src/ytstudio/demo_data/analytics_metrics.json b/src/ytstudio/demo_data/analytics_metrics.json deleted file mode 100644 index cac1909..0000000 --- a/src/ytstudio/demo_data/analytics_metrics.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "zQnBQ4tB3ZA": { - "views": 106666, "likes": 3266, "comments": 80, "shares": 1633, - "estimatedMinutesWatched": 213333, "averageViewDuration": 135, - "averageViewPercentage": 62.5, "subscribersGained": 490, "subscribersLost": 49 - }, - "lHhRhPV--G0": { - "views": 70000, "likes": 2400, "comments": 60, "shares": 1200, - "estimatedMinutesWatched": 140000, "averageViewDuration": 135, - "averageViewPercentage": 62.5, "subscribersGained": 360, "subscribersLost": 36 - }, - "rf60MejMz3E": { - "views": 60000, "likes": 2166, "comments": 50, "shares": 1083, - "estimatedMinutesWatched": 120000, "averageViewDuration": 135, - "averageViewPercentage": 62.5, "subscribersGained": 325, "subscribersLost": 32 - }, - "Ata9cSC2WpM": { - "views": 150000, "likes": 4166, "comments": 106, "shares": 2083, - "estimatedMinutesWatched": 300000, "averageViewDuration": 135, - "averageViewPercentage": 62.5, "subscribersGained": 625, "subscribersLost": 62 - }, - "w7ejDZ8SWv8": { - "views": 226666, "likes": 7166, "comments": 283, "shares": 3583, - "estimatedMinutesWatched": 453333, "averageViewDuration": 135, - "averageViewPercentage": 62.5, "subscribersGained": 1075, "subscribersLost": 107 - }, - "dQw4w9WgXcQ": { - "views": 173000, "likes": 5540, "comments": 155, "shares": 2770, - "estimatedMinutesWatched": 346000, "averageViewDuration": 128, - "averageViewPercentage": 59.3, "subscribersGained": 780, "subscribersLost": 78 - }, - "7C2z4GqqS5E": { - "views": 93000, "likes": 2976, "comments": 74, "shares": 1488, - "estimatedMinutesWatched": 195300, "averageViewDuration": 142, - "averageViewPercentage": 65.7, "subscribersGained": 418, "subscribersLost": 41 - }, - "8C3z4GqqS5F": { - "views": 130000, "likes": 5200, "comments": 210, "shares": 2340, - "estimatedMinutesWatched": 390000, "averageViewDuration": 180, - "averageViewPercentage": 55.2, "subscribersGained": 585, "subscribersLost": 58 - }, - "9D4z5HrrT6G": { - "views": 80000, "likes": 2640, "comments": 62, "shares": 1280, - "estimatedMinutesWatched": 160000, "averageViewDuration": 131, - "averageViewPercentage": 61.8, "subscribersGained": 352, "subscribersLost": 35 - }, - "0E5z6IssU7H": { - "views": 63000, "likes": 2016, "comments": 48, "shares": 945, - "estimatedMinutesWatched": 132300, "averageViewDuration": 138, - "averageViewPercentage": 63.9, "subscribersGained": 283, "subscribersLost": 28 - }, - "1F6z7JttV8I": { - "views": 53000, "likes": 1749, "comments": 42, "shares": 795, - "estimatedMinutesWatched": 106000, "averageViewDuration": 126, - "averageViewPercentage": 58.1, "subscribersGained": 238, "subscribersLost": 23 - }, - "2G7z8KuuW9J": { - "views": 36000, "likes": 1440, "comments": 68, "shares": 720, - "estimatedMinutesWatched": 108000, "averageViewDuration": 195, - "averageViewPercentage": 52.8, "subscribersGained": 162, "subscribersLost": 16 - }, - "3H8z9LvvX0K": { - "views": 70000, "likes": 2310, "comments": 56, "shares": 1050, - "estimatedMinutesWatched": 140000, "averageViewDuration": 132, - "averageViewPercentage": 60.9, "subscribersGained": 315, "subscribersLost": 31 - }, - "4I9z0MwwY1L": { - "views": 86000, "likes": 2838, "comments": 69, "shares": 1376, - "estimatedMinutesWatched": 180600, "averageViewDuration": 140, - "averageViewPercentage": 64.4, "subscribersGained": 387, "subscribersLost": 38 - }, - "5J0z1NxxZ2M": { - "views": 29000, "likes": 1218, "comments": 95, "shares": 580, - "estimatedMinutesWatched": 72500, "averageViewDuration": 168, - "averageViewPercentage": 56.4, "subscribersGained": 145, "subscribersLost": 14 - }, - "6K1z2OyyA3N": { - "views": 60000, "likes": 1920, "comments": 46, "shares": 900, - "estimatedMinutesWatched": 126000, "averageViewDuration": 134, - "averageViewPercentage": 61.2, "subscribersGained": 270, "subscribersLost": 27 - }, - "7L2z3PzzB4O": { - "views": 103000, "likes": 3296, "comments": 82, "shares": 1545, - "estimatedMinutesWatched": 206000, "averageViewDuration": 130, - "averageViewPercentage": 60.4, "subscribersGained": 463, "subscribersLost": 46 - }, - "8M3z4QaaC5P": { - "views": 50000, "likes": 1650, "comments": 40, "shares": 750, - "estimatedMinutesWatched": 105000, "averageViewDuration": 133, - "averageViewPercentage": 61.5, "subscribersGained": 225, "subscribersLost": 22 - }, - "9N4z5RbbD6Q": { - "views": 46000, "likes": 1518, "comments": 36, "shares": 690, - "estimatedMinutesWatched": 100280, "averageViewDuration": 144, - "averageViewPercentage": 66.2, "subscribersGained": 207, "subscribersLost": 20 - }, - "0O5z6SccE7R": { - "views": 56000, "likes": 1848, "comments": 44, "shares": 840, - "estimatedMinutesWatched": 117600, "averageViewDuration": 131, - "averageViewPercentage": 60.5, "subscribersGained": 252, "subscribersLost": 25 - }, - "countries": ["US", "IN", "GB", "DE", "BR"] -} diff --git a/src/ytstudio/demo_data/broadcasts.json b/src/ytstudio/demo_data/broadcasts.json deleted file mode 100644 index 32308f5..0000000 --- a/src/ytstudio/demo_data/broadcasts.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "items": [ - { - "id": "demo-broadcast-upcoming-1", - "snippet": { - "title": "Demo upcoming livestream", - "description": "Scheduled demo broadcast (offline preview)", - "scheduledStartTime": "2026-06-15T19:00:00Z", - "scheduledEndTime": "2026-06-15T20:30:00Z" - }, - "status": { - "lifeCycleStatus": "ready", - "privacyStatus": "public", - "selfDeclaredMadeForKids": false - }, - "contentDetails": { - "boundStreamId": "demo-stream-1", - "enableAutoStart": true, - "enableAutoStop": true, - "enableDvr": true, - "enableEmbed": true, - "recordFromStart": true, - "closedCaptionsType": "closedCaptionsDisabled", - "latencyPreference": "low", - "projection": "rectangular", - "monitorStream": {"enableMonitorStream": true, "broadcastStreamDelayMs": 0} - } - }, - { - "id": "demo-broadcast-active-1", - "snippet": { - "title": "Demo broadcast in progress", - "description": "Currently live demo broadcast", - "scheduledStartTime": "2026-05-20T10:00:00Z", - "actualStartTime": "2026-05-20T10:02:11Z" - }, - "status": { - "lifeCycleStatus": "live", - "privacyStatus": "unlisted", - "selfDeclaredMadeForKids": false - }, - "contentDetails": { - "boundStreamId": "demo-stream-2", - "enableAutoStart": false, - "enableAutoStop": false, - "enableDvr": true, - "enableEmbed": true, - "recordFromStart": false, - "closedCaptionsType": "closedCaptionsEmbedded", - "latencyPreference": "normal", - "projection": "rectangular", - "monitorStream": {"enableMonitorStream": true, "broadcastStreamDelayMs": 5000} - } - }, - { - "id": "demo-broadcast-completed-1", - "snippet": { - "title": "Demo broadcast already finished", - "description": "Past demo broadcast", - "scheduledStartTime": "2026-04-01T18:00:00Z", - "actualStartTime": "2026-04-01T18:01:00Z", - "actualEndTime": "2026-04-01T19:30:00Z" - }, - "status": { - "lifeCycleStatus": "complete", - "privacyStatus": "public", - "selfDeclaredMadeForKids": false - }, - "contentDetails": { - "boundStreamId": "demo-stream-1", - "enableAutoStart": false, - "enableAutoStop": false, - "enableDvr": true, - "enableEmbed": false, - "recordFromStart": true, - "closedCaptionsType": "closedCaptionsDisabled", - "latencyPreference": "normal", - "projection": "rectangular", - "monitorStream": {"enableMonitorStream": false, "broadcastStreamDelayMs": 0} - } - } - ] -} diff --git a/src/ytstudio/demo_data/channel.json b/src/ytstudio/demo_data/channel.json deleted file mode 100644 index 509f879..0000000 --- a/src/ytstudio/demo_data/channel.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "items": [ - { - "id": "UCsBjURrPoezykLs9EqgamOA", - "snippet": { - "title": "Fireship", - "customUrl": "@Fireship" - }, - "statistics": { - "subscriberCount": "4060000", - "viewCount": "654000000", - "videoCount": "850" - }, - "contentDetails": { - "relatedPlaylists": { - "uploads": "UUsBjURrPoezykLs9EqgamOA" - } - } - } - ] -} diff --git a/src/ytstudio/demo_data/comments.json b/src/ytstudio/demo_data/comments.json deleted file mode 100644 index bb667c3..0000000 --- a/src/ytstudio/demo_data/comments.json +++ /dev/null @@ -1,284 +0,0 @@ -{ - "items": [ - { - "id": "Ugw1abc123", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "CodeNewbie", - "textOriginal": "This is the best explanation I've ever seen!", - "likeCount": 1542, - "publishedAt": "2026-01-15T10:00:00Z", - "videoId": "zQnBQ4tB3ZA" - } - } - } - }, - { - "id": "Ugw2def456", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "DevSenior", - "textOriginal": "100 seconds well spent. Subscribed!", - "likeCount": 856, - "publishedAt": "2026-01-15T07:00:00Z", - "videoId": "Ata9cSC2WpM" - } - } - } - }, - { - "id": "Ugw3ghi789", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "EarnBigNow", - "textOriginal": "I make $5,000/week from home! Visit my profile for the FREE method", - "likeCount": 0, - "publishedAt": "2026-01-15T04:00:00Z", - "videoId": "zQnBQ4tB3ZA" - } - } - } - }, - { - "id": "Ugw4jkl012", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "JuniorDev2024", - "textOriginal": "Finally understand this after watching 10 other tutorials", - "likeCount": 634, - "publishedAt": "2026-01-14T12:00:00Z", - "videoId": "zQnBQ4tB3ZA" - } - } - } - }, - { - "id": "Ugw5mno345", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "CryptoKing99", - "textOriginal": "CHECK OUT MY CHANNEL for daily crypto signals!! 100% guaranteed gains!!!", - "likeCount": 0, - "publishedAt": "2026-01-14T12:00:00Z", - "videoId": "dQw4w9WgXcQ" - } - } - } - }, - { - "id": "Ugw6pqr678", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "RustEvangelist", - "textOriginal": "The borrow checker is your friend, not your enemy. Great intro!", - "likeCount": 1203, - "publishedAt": "2026-01-14T09:30:00Z", - "videoId": "7C2z4GqqS5E" - } - } - } - }, - { - "id": "Ugw7stu901", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "CloudArchitect", - "textOriginal": "I wish someone showed me Docker this clearly 5 years ago", - "likeCount": 742, - "publishedAt": "2026-01-13T22:15:00Z", - "videoId": "9D4z5HrrT6G" - } - } - } - }, - { - "id": "Ugw8vwx234", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "DataEngineer42", - "textOriginal": "SQL is still king in 2026. This aged perfectly.", - "likeCount": 489, - "publishedAt": "2026-01-13T18:45:00Z", - "videoId": "3H8z9LvvX0K" - } - } - } - }, - { - "id": "Ugw9yza567", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "LinuxGuru", - "textOriginal": "btw I use Arch. But seriously, great summary.", - "likeCount": 1876, - "publishedAt": "2026-01-13T15:00:00Z", - "videoId": "4I9z0MwwY1L" - } - } - } - }, - { - "id": "Ugw10bcd890", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "FrontendWizard", - "textOriginal": "The CSS container queries trick blew my mind", - "likeCount": 367, - "publishedAt": "2026-01-13T11:20:00Z", - "videoId": "2G7z8KuuW9J" - } - } - } - }, - { - "id": "Ugw11efg123", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "StartupFounder", - "textOriginal": "Showed this to my entire team. Next.js adoption starts Monday.", - "likeCount": 523, - "publishedAt": "2026-01-12T20:30:00Z", - "videoId": "6K1z2OyyA3N" - } - } - } - }, - { - "id": "Ugw12hij456", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "GitMaster", - "textOriginal": "git rebase --interactive is the real superpower nobody talks about", - "likeCount": 934, - "publishedAt": "2026-01-12T16:00:00Z", - "videoId": "7L2z3PzzB4O" - } - } - } - }, - { - "id": "Ugw13klm789", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "BackendDev", - "textOriginal": "MongoDB is great until you need joins. Then you cry.", - "likeCount": 2105, - "publishedAt": "2026-01-12T12:45:00Z", - "videoId": "8M3z4QaaC5P" - } - } - } - }, - { - "id": "Ugw14nop012", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "DevOpsNinja", - "textOriginal": "K8s in 100 seconds but it takes 100 days to master", - "likeCount": 1458, - "publishedAt": "2026-01-11T21:00:00Z", - "videoId": "0E5z6IssU7H" - } - } - } - }, - { - "id": "Ugw15qrs345", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "AIResearcher", - "textOriginal": "The pace of AI progress is genuinely hard to keep up with", - "likeCount": 312, - "publishedAt": "2026-01-11T14:30:00Z", - "videoId": "5J0z1NxxZ2M" - } - } - } - }, - { - "id": "Ugw16tuv678", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "WebDevDaily", - "textOriginal": "Svelte compiles away the framework. Still blows my mind every time.", - "likeCount": 678, - "publishedAt": "2026-01-11T08:15:00Z", - "videoId": "0O5z6SccE7R" - } - } - } - }, - { - "id": "Ugw17wxy901", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "Pythonista", - "textOriginal": "Python: where indentation is not a style choice, it's the law", - "likeCount": 1789, - "publishedAt": "2026-01-10T19:00:00Z", - "videoId": "dQw4w9WgXcQ" - } - } - } - }, - { - "id": "Ugw18zab234", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "FrameworkHopper", - "textOriginal": "Built the same app 10 times and still can't decide which framework to use", - "likeCount": 2567, - "publishedAt": "2026-01-10T13:45:00Z", - "videoId": "8C3z4GqqS5F" - } - } - } - }, - { - "id": "Ugw19cde567", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "AWSCertified", - "textOriginal": "My AWS bill after following this tutorial: $0.03. Worth it.", - "likeCount": 445, - "publishedAt": "2026-01-10T07:30:00Z", - "videoId": "9N4z5RbbD6Q" - } - } - } - }, - { - "id": "Ugw20fgh890", - "snippet": { - "topLevelComment": { - "snippet": { - "authorDisplayName": "FlutterDev", - "textOriginal": "Hot reload alone makes Flutter worth learning", - "likeCount": 398, - "publishedAt": "2026-01-09T16:00:00Z", - "videoId": "lHhRhPV--G0" - } - } - } - } - ] -} diff --git a/src/ytstudio/demo_data/live_streams.json b/src/ytstudio/demo_data/live_streams.json deleted file mode 100644 index 494ce66..0000000 --- a/src/ytstudio/demo_data/live_streams.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "items": [ - { - "id": "demo-stream-1", - "snippet": {"title": "Demo reusable stream"}, - "cdn": { - "format": "1080p", - "frameRate": "30fps", - "resolution": "1080p", - "ingestionInfo": { - "ingestionAddress": "rtmp://a.rtmp.youtube.com/live2", - "backupIngestionAddress": "rtmp://b.rtmp.youtube.com/live2?backup=1", - "rtmpsIngestionAddress": "rtmps://a.rtmps.youtube.com/live2", - "streamName": "demo-key-1234-abcd-zxcv-XYZ9" - } - }, - "status": {"streamStatus": "active"} - }, - { - "id": "demo-stream-2", - "snippet": {"title": "Demo backup stream"}, - "cdn": { - "format": "720p", - "frameRate": "60fps", - "resolution": "720p", - "ingestionInfo": { - "ingestionAddress": "rtmp://a.rtmp.youtube.com/live2", - "streamName": "demo-key-9999-zzzz-PEXX" - } - }, - "status": {"streamStatus": "active"} - } - ] -} diff --git a/src/ytstudio/demo_data/playlist_items.json b/src/ytstudio/demo_data/playlist_items.json deleted file mode 100644 index 3f468c5..0000000 --- a/src/ytstudio/demo_data/playlist_items.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "items": [ - {"snippet": {"title": "TypeScript in 100 Seconds", "publishedAt": "2020-11-25T00:00:00Z"}, "contentDetails": {"videoId": "zQnBQ4tB3ZA"}}, - {"snippet": {"title": "Flutter in 100 Seconds", "publishedAt": "2020-04-14T00:00:00Z"}, "contentDetails": {"videoId": "lHhRhPV--G0"}}, - {"snippet": {"title": "Recursion in 100 Seconds", "publishedAt": "2019-12-30T00:00:00Z"}, "contentDetails": {"videoId": "rf60MejMz3E"}}, - {"snippet": {"title": "React in 100 Seconds", "publishedAt": "2021-05-12T00:00:00Z"}, "contentDetails": {"videoId": "Ata9cSC2WpM"}}, - {"snippet": {"title": "God-Tier Developer Roadmap", "publishedAt": "2022-08-15T00:00:00Z"}, "contentDetails": {"videoId": "w7ejDZ8SWv8"}}, - {"snippet": {"title": "Python in 100 Seconds", "publishedAt": "2021-03-08T00:00:00Z"}, "contentDetails": {"videoId": "dQw4w9WgXcQ"}}, - {"snippet": {"title": "Rust in 100 Seconds", "publishedAt": "2021-07-22T00:00:00Z"}, "contentDetails": {"videoId": "7C2z4GqqS5E"}}, - {"snippet": {"title": "I built the same app 10 times", "publishedAt": "2023-01-18T00:00:00Z"}, "contentDetails": {"videoId": "8C3z4GqqS5F"}}, - {"snippet": {"title": "Docker in 100 Seconds", "publishedAt": "2020-08-03T00:00:00Z"}, "contentDetails": {"videoId": "9D4z5HrrT6G"}}, - {"snippet": {"title": "Kubernetes in 100 Seconds", "publishedAt": "2021-02-15T00:00:00Z"}, "contentDetails": {"videoId": "0E5z6IssU7H"}}, - {"snippet": {"title": "GraphQL in 100 Seconds", "publishedAt": "2020-06-20T00:00:00Z"}, "contentDetails": {"videoId": "1F6z7JttV8I"}}, - {"snippet": {"title": "10 CSS tricks you didn't know", "publishedAt": "2022-11-05T00:00:00Z"}, "contentDetails": {"videoId": "2G7z8KuuW9J"}}, - {"snippet": {"title": "SQL Explained in 100 Seconds", "publishedAt": "2021-09-12T00:00:00Z"}, "contentDetails": {"videoId": "3H8z9LvvX0K"}}, - {"snippet": {"title": "Linux in 100 Seconds", "publishedAt": "2020-10-28T00:00:00Z"}, "contentDetails": {"videoId": "4I9z0MwwY1L"}}, - {"snippet": {"title": "AI is getting satisfying", "publishedAt": "2024-02-01T00:00:00Z"}, "contentDetails": {"videoId": "5J0z1NxxZ2M"}}, - {"snippet": {"title": "Next.js in 100 Seconds", "publishedAt": "2021-11-03T00:00:00Z"}, "contentDetails": {"videoId": "6K1z2OyyA3N"}}, - {"snippet": {"title": "Git in 100 Seconds", "publishedAt": "2020-05-18T00:00:00Z"}, "contentDetails": {"videoId": "7L2z3PzzB4O"}}, - {"snippet": {"title": "MongoDB in 100 Seconds", "publishedAt": "2021-04-07T00:00:00Z"}, "contentDetails": {"videoId": "8M3z4QaaC5P"}}, - {"snippet": {"title": "AWS in 100 Seconds", "publishedAt": "2022-03-14T00:00:00Z"}, "contentDetails": {"videoId": "9N4z5RbbD6Q"}}, - {"snippet": {"title": "Svelte in 100 Seconds", "publishedAt": "2021-06-25T00:00:00Z"}, "contentDetails": {"videoId": "0O5z6SccE7R"}} - ], - "pageInfo": {"totalResults": 20}, - "nextPageToken": null -} diff --git a/src/ytstudio/demo_data/videos.json b/src/ytstudio/demo_data/videos.json deleted file mode 100644 index 6c63910..0000000 --- a/src/ytstudio/demo_data/videos.json +++ /dev/null @@ -1,451 +0,0 @@ -{ - "items": [ - { - "id": "zQnBQ4tB3ZA", - "snippet": { - "title": "TypeScript in 100 Seconds", - "description": "Learn the basics of TypeScript in 100 seconds...", - "publishedAt": "2020-11-25T00:00:00Z", - "tags": ["typescript", "javascript", "100SecondsOfCode", "programming"], - "categoryId": "28", - "defaultLanguage": "en", - "defaultAudioLanguage": "en" - }, - "statistics": { - "viewCount": "3200000", - "likeCount": "98000", - "commentCount": "2400" - }, - "contentDetails": { - "duration": "PT2M1S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": { - "en": { - "title": "TypeScript in 100 Seconds", - "description": "Learn the basics of TypeScript in 100 seconds..." - } - } - }, - { - "id": "lHhRhPV--G0", - "snippet": { - "title": "Flutter in 100 Seconds", - "description": "Build apps on iOS, Android, the web, and desktop with Flutter...", - "publishedAt": "2020-04-14T00:00:00Z", - "tags": ["flutter", "dart", "mobile", "100SecondsOfCode"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "2100000", - "likeCount": "72000", - "commentCount": "1800" - }, - "contentDetails": { - "duration": "PT2M8S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "rf60MejMz3E", - "snippet": { - "title": "Recursion in 100 Seconds", - "description": "Learn how recursion works in 100 seconds...", - "publishedAt": "2019-12-30T00:00:00Z", - "tags": ["recursion", "algorithms", "100SecondsOfCode", "compsci"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "1800000", - "likeCount": "65000", - "commentCount": "1500" - }, - "contentDetails": { - "duration": "PT1M48S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "Ata9cSC2WpM", - "snippet": { - "title": "React in 100 Seconds", - "description": "Learn the basics of React in 100 seconds...", - "publishedAt": "2021-05-12T00:00:00Z", - "tags": ["react", "javascript", "frontend", "100SecondsOfCode"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "4500000", - "likeCount": "125000", - "commentCount": "3200" - }, - "contentDetails": { - "duration": "PT2M15S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "w7ejDZ8SWv8", - "snippet": { - "title": "God-Tier Developer Roadmap", - "description": "The mass extinction satisfies both business and our lizard brain...", - "publishedAt": "2022-08-15T00:00:00Z", - "tags": ["roadmap", "developer", "career", "programming"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "6800000", - "likeCount": "215000", - "commentCount": "8500" - }, - "contentDetails": { - "duration": "PT11M42S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "dQw4w9WgXcQ", - "snippet": { - "title": "Python in 100 Seconds", - "description": "Learn Python in 100 seconds...", - "publishedAt": "2021-03-08T00:00:00Z", - "tags": ["python", "programming", "100SecondsOfCode", "backend"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "5200000", - "likeCount": "142000", - "commentCount": "4100" - }, - "contentDetails": { - "duration": "PT2M12S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "7C2z4GqqS5E", - "snippet": { - "title": "Rust in 100 Seconds", - "description": "Rust is a blazingly fast systems programming language...", - "publishedAt": "2021-07-22T00:00:00Z", - "tags": ["rust", "systems", "100SecondsOfCode", "programming"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "2800000", - "likeCount": "89000", - "commentCount": "2200" - }, - "contentDetails": { - "duration": "PT2M24S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "8C3z4GqqS5F", - "snippet": { - "title": "I built the same app 10 times", - "description": "Which JavaScript framework is best?...", - "publishedAt": "2023-01-18T00:00:00Z", - "tags": ["frameworks", "comparison", "webdev", "javascript"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "3900000", - "likeCount": "156000", - "commentCount": "5600" - }, - "contentDetails": { - "duration": "PT14M33S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "9D4z5HrrT6G", - "snippet": { - "title": "Docker in 100 Seconds", - "description": "Containerize your applications with Docker...", - "publishedAt": "2020-08-03T00:00:00Z", - "tags": ["docker", "devops", "100SecondsOfCode", "containers"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "2400000", - "likeCount": "78000", - "commentCount": "1900" - }, - "contentDetails": { - "duration": "PT2M6S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "0E5z6IssU7H", - "snippet": { - "title": "Kubernetes in 100 Seconds", - "description": "Orchestrate containers at scale with K8s...", - "publishedAt": "2021-02-15T00:00:00Z", - "tags": ["kubernetes", "k8s", "devops", "100SecondsOfCode"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "1900000", - "likeCount": "62000", - "commentCount": "1400" - }, - "contentDetails": { - "duration": "PT2M18S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "1F6z7JttV8I", - "snippet": { - "title": "GraphQL in 100 Seconds", - "description": "A query language for your API...", - "publishedAt": "2020-06-20T00:00:00Z", - "tags": ["graphql", "api", "100SecondsOfCode", "backend"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "1600000", - "likeCount": "54000", - "commentCount": "1200" - }, - "contentDetails": { - "duration": "PT2M4S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "2G7z8KuuW9J", - "snippet": { - "title": "10 CSS tricks you didn't know", - "description": "Level up your CSS game with these tips...", - "publishedAt": "2022-11-05T00:00:00Z", - "tags": ["css", "frontend", "webdev", "tricks"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "1100000", - "likeCount": "45000", - "commentCount": "890" - }, - "contentDetails": { - "duration": "PT8M22S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "3H8z9LvvX0K", - "snippet": { - "title": "SQL Explained in 100 Seconds", - "description": "The most important language for data...", - "publishedAt": "2021-09-12T00:00:00Z", - "tags": ["sql", "database", "100SecondsOfCode", "backend"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "2100000", - "likeCount": "71000", - "commentCount": "1600" - }, - "contentDetails": { - "duration": "PT2M9S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "4I9z0MwwY1L", - "snippet": { - "title": "Linux in 100 Seconds", - "description": "The operating system that runs the world...", - "publishedAt": "2020-10-28T00:00:00Z", - "tags": ["linux", "os", "100SecondsOfCode", "devops"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "2600000", - "likeCount": "82000", - "commentCount": "2100" - }, - "contentDetails": { - "duration": "PT2M15S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "5J0z1NxxZ2M", - "snippet": { - "title": "AI is getting satisfying", - "description": "This week in AI and code...", - "publishedAt": "2024-02-01T00:00:00Z", - "tags": ["ai", "code", "news", "tech"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "890000", - "likeCount": "38000", - "commentCount": "1200" - }, - "contentDetails": { - "duration": "PT5M44S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "6K1z2OyyA3N", - "snippet": { - "title": "Next.js in 100 Seconds", - "description": "The React framework for production...", - "publishedAt": "2021-11-03T00:00:00Z", - "tags": ["nextjs", "react", "100SecondsOfCode", "fullstack"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "1800000", - "likeCount": "58000", - "commentCount": "1300" - }, - "contentDetails": { - "duration": "PT2M11S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "7L2z3PzzB4O", - "snippet": { - "title": "Git in 100 Seconds", - "description": "Version control for the modern developer...", - "publishedAt": "2020-05-18T00:00:00Z", - "tags": ["git", "vcs", "100SecondsOfCode", "devtools"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "3100000", - "likeCount": "95000", - "commentCount": "2300" - }, - "contentDetails": { - "duration": "PT2M3S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "8M3z4QaaC5P", - "snippet": { - "title": "MongoDB in 100 Seconds", - "description": "A document database built for modern apps...", - "publishedAt": "2021-04-07T00:00:00Z", - "tags": ["mongodb", "nosql", "100SecondsOfCode", "database"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "1500000", - "likeCount": "49000", - "commentCount": "1100" - }, - "contentDetails": { - "duration": "PT2M7S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "9N4z5RbbD6Q", - "snippet": { - "title": "AWS in 100 Seconds", - "description": "The cloud platform that powers the internet...", - "publishedAt": "2022-03-14T00:00:00Z", - "tags": ["aws", "cloud", "100SecondsOfCode", "devops"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "1400000", - "likeCount": "46000", - "commentCount": "980" - }, - "contentDetails": { - "duration": "PT2M19S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - }, - { - "id": "0O5z6SccE7R", - "snippet": { - "title": "Svelte in 100 Seconds", - "description": "A radical new approach to building UIs...", - "publishedAt": "2021-06-25T00:00:00Z", - "tags": ["svelte", "frontend", "100SecondsOfCode", "javascript"], - "categoryId": "28" - }, - "statistics": { - "viewCount": "1700000", - "likeCount": "56000", - "commentCount": "1250" - }, - "contentDetails": { - "duration": "PT2M5S" - }, - "status": { - "privacyStatus": "public" - }, - "localizations": {} - } - ] -} diff --git a/src/ytstudio/services.py b/src/ytstudio/services.py index 2873af9..6a152b4 100644 --- a/src/ytstudio/services.py +++ b/src/ytstudio/services.py @@ -1,14 +1,9 @@ from ytstudio.api import get_authenticated_service -from ytstudio.demo import DemoAnalyticsService, DemoDataService, is_demo_mode def get_data_service(profile: str | None = None): - if is_demo_mode(): - return DemoDataService() return get_authenticated_service("youtube", "v3", profile=profile) def get_analytics_service(profile: str | None = None): - if is_demo_mode(): - return DemoAnalyticsService() return get_authenticated_service("youtubeAnalytics", "v2", profile=profile) diff --git a/tests/conftest.py b/tests/conftest.py index 5f3d302..836cb91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,7 +27,7 @@ "title": "Test Video Title", "description": "This is a test video description that is long enough.", "publishedAt": "2026-01-15T10:00:00Z", - "tags": ["test", "video", "youtube", "cli", "demo"], + "tags": ["test", "video", "youtube", "cli"], "categoryId": "22", }, "statistics": { diff --git a/tests/test_analytics.py b/tests/test_analytics.py index f12e5c1..1d1c2c6 100644 --- a/tests/test_analytics.py +++ b/tests/test_analytics.py @@ -38,10 +38,64 @@ def test_raw_mode(self): class TestAnalyticsCommands: - def test_overview(self): - with patch("ytstudio.services.is_demo_mode", return_value=True): + def _mock_overview_services(self): + data_service = MagicMock() + analytics_service = MagicMock() + data_service.channels.return_value.list.return_value.execute.return_value = { + "items": [{"id": "UC_test"}] + } + analytics_service.reports.return_value.query.return_value.execute.return_value = { + "columnHeaders": [ + {"name": "views"}, + {"name": "estimatedMinutesWatched"}, + {"name": "averageViewDuration"}, + {"name": "subscribersGained"}, + {"name": "subscribersLost"}, + {"name": "likes"}, + {"name": "comments"}, + ], + "rows": [[12345, 6000, 180, 42, 3, 789, 25]], + } + return data_service, analytics_service + + def test_overview_table(self): + data_svc, analytics_svc = self._mock_overview_services() + with ( + patch("ytstudio.commands.analytics.get_data_service", return_value=data_svc), + patch("ytstudio.commands.analytics.get_analytics_service", return_value=analytics_svc), + ): + result = runner.invoke(app, ["analytics", "overview"]) + assert result.exit_code == 0 + assert "Channel Analytics" in result.output + assert "12.3K" in result.output or "12345" in result.output + assert "100 hours" in result.output + + def test_overview_json(self): + data_svc, analytics_svc = self._mock_overview_services() + with ( + patch("ytstudio.commands.analytics.get_data_service", return_value=data_svc), + patch("ytstudio.commands.analytics.get_analytics_service", return_value=analytics_svc), + ): + result = runner.invoke(app, ["analytics", "overview", "-o", "json"]) + assert result.exit_code == 0 + payload = json.loads(result.output) + assert payload["days"] == 28 + assert payload["analytics"]["views"] == 12345 + assert payload["analytics"]["likes"] == 789 + + def test_overview_no_data(self): + data_svc, analytics_svc = self._mock_overview_services() + analytics_svc.reports.return_value.query.return_value.execute.return_value = { + "columnHeaders": [{"name": "views"}], + "rows": [], + } + with ( + patch("ytstudio.commands.analytics.get_data_service", return_value=data_svc), + patch("ytstudio.commands.analytics.get_analytics_service", return_value=analytics_svc), + ): result = runner.invoke(app, ["analytics", "overview"]) assert result.exit_code == 0 + assert "No analytics data" in result.output def test_video_not_found(self, mock_auth): mock_auth.videos.return_value.list.return_value.execute.return_value = {"items": []} @@ -178,39 +232,6 @@ def test_query_invalid_filter_format(self): assert "Invalid filter" in result.output -class TestQueryDemoMode: - def test_query_demo_table(self): - with patch("ytstudio.services.is_demo_mode", return_value=True): - result = runner.invoke( - app, - ["analytics", "query", "-m", "views,likes", "-d", "day", "--days", "7"], - ) - assert result.exit_code == 0 - assert "views" in result.output - assert "likes" in result.output - - def test_query_demo_json(self): - with patch("ytstudio.services.is_demo_mode", return_value=True): - result = runner.invoke( - app, - ["analytics", "query", "-m", "views,likes", "-d", "country", "-o", "json"], - ) - assert result.exit_code == 0 - data = json.loads(result.output) - assert len(data) > 0 - assert "views" in data[0] - assert "country" in data[0] - - def test_query_demo_no_dimensions(self): - with patch("ytstudio.services.is_demo_mode", return_value=True): - result = runner.invoke( - app, - ["analytics", "query", "-m", "views,likes"], - ) - assert result.exit_code == 0 - assert "views" in result.output - - class TestMetricsCommand: def test_list_all(self): result = runner.invoke(app, ["analytics", "metrics"]) diff --git a/tests/test_livestreams.py b/tests/test_livestreams.py index d742fb6..b9512aa 100644 --- a/tests/test_livestreams.py +++ b/tests/test_livestreams.py @@ -45,7 +45,7 @@ def _broadcast_item( "id": broadcast_id, "snippet": { "title": title, - "description": "Demo", + "description": "Sample", "scheduledStartTime": scheduled_start, "scheduledEndTime": scheduled_end, }, diff --git a/tests/test_upload_command.py b/tests/test_upload_command.py index 1738d7e..fefebd6 100644 --- a/tests/test_upload_command.py +++ b/tests/test_upload_command.py @@ -14,13 +14,13 @@ description: | Hello privacy: private -tags: [demo] +tags: [sample] """ def _stage(tmp_path: Path) -> Path: - (tmp_path / "demo.mp4").write_bytes(b"fake-video") - (tmp_path / "demo.yaml").write_text(SIDECAR) + (tmp_path / "sample.mp4").write_bytes(b"fake-video") + (tmp_path / "sample.yaml").write_text(SIDECAR) return tmp_path @@ -31,7 +31,7 @@ def test_upload_dry_run_lists_jobs_and_does_not_call_api(tmp_path, mock_auth): assert result.exit_code == 0 assert "Dry Run Sample" in result.stdout - assert "demo.mp4" in result.stdout + assert "sample.mp4" in result.stdout assert "private" in result.stdout mock_auth.videos.return_value.insert.assert_not_called() @@ -61,14 +61,14 @@ def test_upload_execute_uploads_and_writes_back(tmp_path, mock_auth): assert result.exit_code == 0 mock_auth.videos.return_value.insert.assert_called_once() - sidecar_text = (tmp_path / "demo.yaml").read_text() + sidecar_text = (tmp_path / "sample.yaml").read_text() assert "video_id: vid1" in sidecar_text assert "uploaded_at:" in sidecar_text def test_upload_execute_skips_already_uploaded(tmp_path, mock_auth): _stage(tmp_path) - (tmp_path / "demo.yaml").write_text(SIDECAR + "\nvideo_id: already-there\n") + (tmp_path / "sample.yaml").write_text(SIDECAR + "\nvideo_id: already-there\n") with patch("ytstudio.upload_pipeline.MediaFileUpload"): result = runner.invoke(app, ["videos", "upload", str(tmp_path), "--execute"]) @@ -123,7 +123,7 @@ def test_upload_execute_stops_on_quota_exceeded(tmp_path, mock_auth): assert "quota" in result.stdout.lower() or "quota" in result.stderr.lower() # First sidecar should be patched, second not. - assert "video_id: v-ok" in (tmp_path / "demo.yaml").read_text() + assert "video_id: v-ok" in (tmp_path / "sample.yaml").read_text() assert "video_id:" not in (tmp_path / "second.yaml").read_text() # Final summary must still print after quota stop (clean-stop behavior). assert "Done: 1/2 uploaded" in result.stdout diff --git a/tests/test_upload_pipeline.py b/tests/test_upload_pipeline.py index 054c643..45c409f 100644 --- a/tests/test_upload_pipeline.py +++ b/tests/test_upload_pipeline.py @@ -84,7 +84,7 @@ def _write(p: Path, body: str = "") -> Path: SIDECAR_OK = """\ title: Sample description: | - Demo body + Sample body """ @@ -271,7 +271,7 @@ def test_body_includes_languages_and_tags(): # Master comment title: Sample # inline comment description: | - Demo + Sample tags: [a, b] """