From 949f576fec759593bd9f6b9017ba0bc5ec0840b2 Mon Sep 17 00:00:00 2001 From: Glyphscribe Ety Date: Sun, 27 Apr 2025 12:11:34 -0500 Subject: [PATCH] add channel ID, expand readme --- README.md | 42 +++++++++++++++++++++++++++++++++++++++++- config.py | 2 +- helpers/clips.py | 36 +++++++++++++++++++++++++++++------- main.py | 1 + 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e8aa682..79e7416 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,39 @@ # owncast-clipper WIP: a bot that listens for clip requests from owncast chat and uploads to a configured peertube instance. +## ✅ Preparation +Beyond easily-discoverable information such as user credentials and base URLs, there are two pieces of information needed which non-technical users may struggle with: the Owncast Access Token and the PeerTube channel ID. + +### Owncast Access Token +Can be found by visiting the `Integrations -> Acess Tokens` page of the Owncast Admin at `admin/access-tokens/`: + +1. Visit the Access Tokens page of your Owncast Admin interface. If your instance is `https://stream.person.site`, the page may be found at `https://stream.person.site/admin/access-token`. +2. You'll need to Create an Access Token and name it appropriately (something like "Clip Bot" is a good starting point). It will require the "Can send chat messages on behalf of the owner of this token" permission. +3. Once the token is created, you'll be able to retrieve it by clicking on the eye icon to unhide the token, then copying and pasting into the `OWNCAST_AUTH` variable as described [below](#⚙️-configuration). + +### PeerTube Channel ID +This one is a little trickier to get, because the value that we need isn't directly exposed via the UI. Here's what to do: +1. Go to your Channel (not _User_) page on PeerTube. If your PeerTube instance is `https://peertube.video.site` and your channel name is `Cool Channel`, you should have a URL like this: `https://peertube.video.site/c/cool_channel/videos`. +2. You want the part of the URL between the `/c/` and `/videos`: in this case: `cool_channel`. +3. Adjust the following URL to match your environment and open it in your browser: `https://peertube.video.site/api/v1/video-channels/cool_channel`. +4. Your browser may or may not format the raw data, but you're looking for the _first_ instance of "id" in the response. If your browser is showing you raw data, you can just do a text search for "id". +5. The number immediately following "id" is your Channel ID! Copy and paste it into your `PEERTUBE_CHANNEL_ID` variable as described [below](#⚙️-configuration). + +## ⚙️ Configuration +At the moment, configuration is performed exclusively through environment variables. + +| Variable | Description | Required | +| :------- | :---------- | :------: | +| PEERTUBE_HOST | The base URL for the PeerTube server we're uploading to, e.g. https://peertube.video.site | **Yes** | +| PEERTUBE_USER | The username of the account authorized to post to PeerTube | **Yes** | +| PEERTUBE_PASS | The password of the account authorized to post to PeerTube | **Yes** | +| PEERTUBE_CHANNEL_ID | The internal "channel id" of the PeerTube channel we're uploading clips to. Described [above](#peertube-channel-id). | **Yes** | +| OWNCAST_HOST | The base URL for the PeerTube server we're making API calls against, e.g. `https://coolstream.site` or `http://app:8080` | **Yes** | +| OWNCAST_AUTH | A valid Owncast Access Token, with access to the User Chat scope. Described [above](#owncast-access-token). | **Yes** | +| CLIP_DIRECTORY | The directory where source .ts files from owncast will be collected. /var/data/hls/0/ by default. | No | +| TEMP_DIRECTORY | The directory where intermediate remuxed .mp4 files will be stored. /tmp/ by default. | No | +| CLIP_QUANTITY | The maximum number of segment files to attempt to collect to make a clip. 20 by default. | No | + ## Example docker-compose.yml ```yaml version: '3' @@ -29,6 +62,13 @@ services: - PEERTUBE_HOST=https://video.peertube.site # your PeerTube server - PEERTUBE_USER=your_peertube_username - PEERTUBE_PASS=SuperSecretPassword + - PEERTUBE_CHANNEL_ID=1502 - OWNCAST_HOST=http://owncast:8080 # internal or fully-qualified Owncast server - OWNCAST_AUTH=AReallyLongOwncastAccessTokenString # Owncast access token with chat permission -``` \ No newline at end of file +``` + + +CLIP_DIRECTORY, TEMP_DIRECTORY, CLIP_QUANTITY = None, None, None +PEERTUBE_HOST, PEERTUBE_USER, PEERTUBE_PASS = None, None, None +OWNCAST_HOST, OWNCAST_AUTH = None, None +AUTHENTICATION_REQUIRED, PT_API_BASE, PEERTUBE_CHANNEL_ID = None, None, None diff --git a/config.py b/config.py index 9ec7e7b..a391b99 100644 --- a/config.py +++ b/config.py @@ -6,4 +6,4 @@ WEBSOCKET_INITIAL_BACKOFF = 5.0 CLIP_DIRECTORY, TEMP_DIRECTORY, CLIP_QUANTITY = None, None, None PEERTUBE_HOST, PEERTUBE_USER, PEERTUBE_PASS = None, None, None OWNCAST_HOST, OWNCAST_AUTH = None, None -AUTHENTICATION_REQUIRED, PT_API_BASE = None, None +AUTHENTICATION_REQUIRED, PT_API_BASE, PEERTUBE_CHANNEL_ID = None, None, None diff --git a/helpers/clips.py b/helpers/clips.py index 4b74e15..a1ae754 100644 --- a/helpers/clips.py +++ b/helpers/clips.py @@ -65,6 +65,27 @@ def merge_segments_to_mp4(segments: list[str], output_path: Path, output_filenam return output_file +def delete_clip(filename: str): + """ + Deletes a named file from the temporary directory + + Args: + filename (str): The name of the file to be deleted, e.g. file.mp4 + + Returns: + None + """ + + file = config.TEMP_DIRECTORY / filename + try: + file.unlink() + except FileNotFoundError: + print(f'Error deleting temporary file: {file}; file not found.') + except Exception as e: + print(f'Error deleting temporary file: {file}; {e}.') + return + + def generate_and_post_clip(user: str, title: str, timestamp: datetime): """ Invokes clip collection, remux, upload, and notification (chat) routines. @@ -78,16 +99,16 @@ def generate_and_post_clip(user: str, title: str, timestamp: datetime): None """ - recent_clips = get_recent_segments( - config.CLIP_DIRECTORY, config.CLIP_QUANTITY) + if not (recent_clips := get_recent_segments(config.CLIP_DIRECTORY, config.CLIP_QUANTITY)): + return new_filename = f'{timestamp.strftime("%Y%m%d_%H%M%S_%f")}.mp4' new_filepath = merge_segments_to_mp4( recent_clips, config.TEMP_DIRECTORY, new_filename) if (token := authenticate_to_peertube(config.PT_API_BASE, config.PEERTUBE_USER, config.PEERTUBE_PASS)) is None: print(f'Failed to authenticate to PeerTube', file=sys.stderr) + delete_clip(new_filename) return - # TODO: handle this gracefully stream_title = '' if (stream_title := get_owncast_title()): @@ -97,14 +118,15 @@ def generate_and_post_clip(user: str, title: str, timestamp: datetime): upload_options[ 'description'] = f'🎞️ Clipped by **{user}** {stream_title} on {timestamp.strftime("%Y-%m-%d %H:%M:%S")}' upload_options['description'] += f'\n🌐 {config.OWNCAST_HOST}' - if (video_url := upload_to_peertube(config.PT_API_BASE, token, new_filepath, 3800, title, **upload_options)) is None: + if (video_url := upload_to_peertube(config.PT_API_BASE, token, new_filepath, config.PEERTUBE_CHANNEL_ID, title, **upload_options)) is None: print(f'Failed to upload file to PeerTube', file=sys.stderr) + delete_clip(new_filename) return - # TODO: handle this gracefully - + delete_clip(new_filename) + clip_url = f'{config.PEERTUBE_HOST}{video_url}' message = f'**{title}**: {clip_url}' send_owncast_message(message) - + print(f'Clip created by {user} at {timestamp}: {clip_url}') return diff --git a/main.py b/main.py index d61ddbf..785705f 100644 --- a/main.py +++ b/main.py @@ -24,6 +24,7 @@ def main(): config.PEERTUBE_HOST = os.environ['PEERTUBE_HOST'].rstrip('/') config.PEERTUBE_USER = os.environ['PEERTUBE_USER'] config.PEERTUBE_PASS = os.environ['PEERTUBE_PASS'] + config.PEERTUBE_CHANNEL_ID = os.environment['PEERTUBE_CHANNEL_ID'] except KeyError as e: print( f'Error: PeerTube host and credentials must be specified: {e}', file=sys.stderr)