mirror of
https://github.com/Dasemu/x-space-downloader.git
synced 2026-10-12 00:18:35 +00:00
chore: polish repo for public release
Add MIT LICENSE, fix README clone URL and add CI/license badges, flesh out setup.py metadata, add a GitHub Actions test workflow, and remove dead code left over from earlier iterations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
25
.github/workflows/tests.yml
vendored
Normal file
25
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt -r requirements-dev.txt
|
||||
- name: Run tests
|
||||
run: pytest -m "not integration"
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Dario Sevilla
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
17
README.md
17
README.md
@@ -1,5 +1,8 @@
|
||||
# X Space Downloader
|
||||
|
||||
[](https://github.com/Dasemu/x-space-downloader/actions/workflows/tests.yml)
|
||||
[](LICENSE)
|
||||
|
||||
A Python CLI tool that downloads X (Twitter) Space recordings and renders an MP4 replay with audio and a compact participant view.
|
||||
|
||||
## Features
|
||||
@@ -35,7 +38,7 @@ sudo apt install ffmpeg
|
||||
### Install the tool
|
||||
|
||||
```bash
|
||||
git clone https://github.com/synapz-org/x-space-downloader.git
|
||||
git clone https://github.com/Dasemu/x-space-downloader.git
|
||||
cd x-space-downloader
|
||||
pip install -e .
|
||||
```
|
||||
@@ -68,6 +71,18 @@ x-space-dl --cookies cookies.txt https://x.com/i/spaces/1ZkKzZWnVRRKv
|
||||
|
||||
Cookies are also used to fetch the Space title, host, speakers, avatars, and replay chat timeline from X/Periscope metadata.
|
||||
|
||||
### Environment variables
|
||||
|
||||
Fetching metadata (required for the default video mode) needs a bearer token for X's web GraphQL API:
|
||||
|
||||
```bash
|
||||
export X_BEARER_TOKEN="<token>"
|
||||
```
|
||||
|
||||
Get this value from your own browser: open x.com while logged in, open DevTools → Network, and copy the `authorization: Bearer ...` header from any request to `x.com/i/api/graphql`. This is the public token issued to X's web client, not a personal secret, but it's not bundled with this tool — you provide your own.
|
||||
|
||||
`X_AUDIO_SPACE_QUERY_ID` can optionally override the GraphQL query ID used for `AudioSpaceById` if X changes it.
|
||||
|
||||
### Speaker timeline
|
||||
|
||||
With cookies, the tool automatically calls the replay `history` endpoint and builds participant/speaker timing from Space microphone events. If you already have speaker intervals from another extractor, you can override that with JSON:
|
||||
|
||||
6
setup.py
6
setup.py
@@ -3,6 +3,12 @@ from setuptools import setup, find_packages
|
||||
setup(
|
||||
name="x-space-downloader",
|
||||
version="0.1.0",
|
||||
description="Download X (Twitter) Space recordings as MP4 replays or MP3 audio",
|
||||
long_description=open("README.md", encoding="utf-8").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://github.com/Dasemu/x-space-downloader",
|
||||
author="Dario Sevilla",
|
||||
license="MIT",
|
||||
packages=find_packages(),
|
||||
install_requires=[
|
||||
"yt-dlp>=2024.0.0",
|
||||
|
||||
@@ -116,13 +116,6 @@ def download_space_video(
|
||||
raise DownloadError(f"Failed to render Space video: {exc}") from exc
|
||||
|
||||
|
||||
def fetch_metadata_or_fallback(space_id: str, cookies_file: Optional[str]) -> SpaceMetadata:
|
||||
try:
|
||||
return XApiClient(cookies_file=cookies_file).fetch_space_metadata(space_id)
|
||||
except XApiError:
|
||||
return SpaceMetadata(space_id=space_id, title=f"X Space {space_id}", participants=[])
|
||||
|
||||
|
||||
def fetch_required_metadata(space_id: str, cookies_file: Optional[str]) -> SpaceMetadata:
|
||||
try:
|
||||
return XApiClient(cookies_file=cookies_file).fetch_space_metadata(space_id)
|
||||
@@ -144,17 +137,6 @@ def fetch_required_timeline(metadata: SpaceMetadata, cookies_file: Optional[str]
|
||||
return SpeakerTimeline(intervals, first_seen)
|
||||
|
||||
|
||||
def fetch_timeline_or_none(metadata: SpaceMetadata, cookies_file: str) -> Optional[SpeakerTimeline]:
|
||||
try:
|
||||
intervals, first_seen, history_participants = XApiClient(cookies_file=cookies_file).fetch_space_timeline(metadata)
|
||||
except XApiError:
|
||||
return None
|
||||
merge_history_participants(metadata, history_participants)
|
||||
if not intervals and not first_seen:
|
||||
return None
|
||||
return SpeakerTimeline(intervals, first_seen)
|
||||
|
||||
|
||||
def merge_history_participants(metadata: SpaceMetadata, history_participants) -> None:
|
||||
existing = {participant.username.lower() for participant in metadata.participants}
|
||||
for participant in history_participants:
|
||||
|
||||
@@ -19,5 +19,5 @@ def validate_space_url(url: str) -> Tuple[bool, Optional[str], Optional[str]]:
|
||||
if not match:
|
||||
return False, None, "Invalid URL format. Expected: https://x.com/i/spaces/{SPACE_ID}"
|
||||
|
||||
space_id = match.group(2) # Changed from group(1) to group(2)
|
||||
space_id = match.group(2)
|
||||
return True, space_id, None
|
||||
|
||||
12
src/video.py
12
src/video.py
@@ -203,8 +203,8 @@ def build_ffmpeg_command(audio_path: str, output_path: str, width: int, height:
|
||||
"veryfast",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-c:a",
|
||||
"copy",
|
||||
"-c:a",
|
||||
"copy",
|
||||
"-shortest",
|
||||
output_path,
|
||||
]
|
||||
@@ -454,11 +454,3 @@ def ellipsize(draw: ImageDraw.ImageDraw, value: str, font, max_width: int) -> st
|
||||
while trimmed and draw.textlength(trimmed + suffix, font=font) > max_width:
|
||||
trimmed = trimmed[:-1]
|
||||
return (trimmed or value[:1]) + suffix
|
||||
|
||||
|
||||
def format_time(seconds: float) -> str:
|
||||
minutes, secs = divmod(int(seconds), 60)
|
||||
hours, minutes = divmod(minutes, 60)
|
||||
if hours:
|
||||
return f"{hours:d}:{minutes:02d}:{secs:02d}"
|
||||
return f"{minutes:d}:{secs:02d}"
|
||||
|
||||
Reference in New Issue
Block a user