Dateien über die Kommandozeile zu YouTube exportieren
Sie können ein fertiggestelltes Video mit Googles Open-Source-Python-Bibliotheken vom Terminal aus hochladen. Die folgende CLI wählt eine lokale Datei und Metadaten-JSON aus, fordert Sie zur Autorisierung eines Kanals auf und gibt die Video-ID zurück, nachdem YouTube den Upload angenommen hat. Videos bleiben privat und Abonnentenbenachrichtigungen sind deaktiviert.
Wichtige Einschränkungen der YouTube-API
Dieses Beispiel dient einem beaufsichtigten Upload unter Linux mit Bash und einem Browser auf demselben Rechner. Es akzeptiert eine nicht leere lokale MP4-Datei bis 64 MiB mit einem UTF-8-Dateinamen. Die Datei muss bereits fertiggestellt und abspielbar sein; das Skript validiert oder transkodiert ihren Inhalt nicht. Insgesamt gilt eine Frist von fünf Minuten einschließlich Autorisierung und Übertragung. Bei langsamer Verbindung kann daher auch eine Datei unterhalb dieses Limits scheitern.
Ein angenommener Upload muss noch verarbeitet werden. Die CLI fordert nur
youtube.upload
an, was das Einfügen erlaubt. Sie fordert nicht die weitergehenden Berechtigungen an, um private
Videos über die API einzusehen oder zu löschen. Prüfen Sie anschließend die zurückgegebene ID in
YouTube Studio des ausgewählten Kanals und warten Sie die Verarbeitung ab, bevor Sie das Video
verwenden.
Prüfen Sie das aktuelle Upload-Kontingent Ihres Projekts in Google Cloud. YouTube rechnet Uploads über ein separates Kontingent ab. Eine Pause zwischen Befehlen füllt ein Tageskontingent nicht wieder auf. Diese Anleitung fordert unabhängig von Projekt- oder Kontovoreinstellungen explizit private Sichtbarkeit an.
Python-Bibliotheken installieren
Verwenden Sie einen installierten CPython-3.14-Interpreter mit verfügbarem
venv und pip. Getestet wurde der Interpreter 3.14.8. Nutzen Sie bei einer
Neuinstallation über die Python-Downloads den aktuellen
Sicherheitspatch der gepflegten 3.14-Linie. Python 3.15 ist neuer, doch diese Anleitung wurde mit
3.14 getestet, das der Google-API-Client ausdrücklich als
unterstützt aufführt.
Führen Sie dies im übergeordneten Verzeichnis aus, in dem ein neues Verzeichnis
youtube-cli entstehen soll. Die Subshell lässt Ihr aktuelles Verzeichnis und
Ihre Shell-Optionen unverändert. Ein vorhandenes Verzeichnis wird abgelehnt. Falls die Installation
scheitert, prüfen Sie den Fehler und das teilweise erstellte Verzeichnis, bevor Sie einen neuen
Speicherort wählen. Die Befehle entfernen keine vorhandenen Dateien.
(
set -eu
command -v python3.14 >/dev/null
mkdir youtube-cli
python3.14 -m venv youtube-cli/.venv
youtube-cli/.venv/bin/python -m pip --isolated install \
google-api-python-client==2.201.0 google-auth-oauthlib==1.5.0 \
google-auth==2.62.1 httplib2==0.32.1 requests==2.34.2
)
Dies sind getestete Paketversionen. Der Google-Client wird mit Fehlerkorrekturen gepflegt und stellt die Discovery-basierte API von YouTube bereit. Die kleine Requests-Transportschicht unten legt das Retry-Verhalten explizit fest: Ein fehlgeschlagenes Einfügen wird niemals automatisch als weiterer Einfügevorgang neu gestartet.
Desktop-OAuth einrichten
Verwenden Sie ein Google-Konto, dem der gewünschte YouTube-Kanal gehört. Folgen Sie Googles Anleitung zur Desktop-OAuth-Einrichtung:
- Wählen Sie Ihr Projekt in der Google Cloud Console aus und aktivieren Sie die YouTube Data API v3.
- Konfigurieren Sie die Einwilligungsinformationen und die Zielgruppe der App. Fügen Sie bei einer externen App im Testbetrieb Ihr Google-Konto als Testnutzer hinzu.
- Erstellen Sie einen OAuth-Client mit dem Anwendungstyp Desktop app.
Laden Sie dessen JSON als
youtube-cli/client_secrets.jsonherunter. Nehmen Sie diese Datei nicht in die Versionsverwaltung auf. - Suchen Sie die ID des gewünschten Kanals in Ihren
erweiterten YouTube-Kontoeinstellungen.
Verwenden Sie diese ID mit
UC…im folgenden Befehl. Ein Handle wie@exampleist keine Kanal-ID.
Das Programm verwendet einen zufälligen Loopback-Port und PKCE. Es fordert bei jedem Aufruf eine neue Einwilligung an, speichert keinen Token-Cache und bittet Sie um Bestätigung des Kanals, bevor es etwas einfügt. Wählen Sie während Googles Autorisierungsablauf den gewünschten Kanal und gegebenenfalls das richtige Brand-Konto. Die Eingabe einer ID wählt keinen Kanal aus: Das autorisierte Konto bestimmt das Ziel. Die zurückgegebene Kanal-ID wird nach dem Einfügen geprüft. Eine Abweichung gilt als Fehler, kann aber bereits ein privates Video im anderen Kanal erzeugt haben.
Video-Metadaten verwalten
Speichern Sie dies als youtube-cli/metadata.json und ersetzen Sie Titel und Beschreibung.
Setzen Sie madeForKids entsprechend Ihrer tatsächlichen Zielgruppe. Diese vier
Schlüssel bilden die vollständige Metadatenschnittstelle; unbekannte Schlüssel werden abgelehnt.
Privatsphäre und Benachrichtigungen sind im Programm festgelegt und werden nicht aus einer
Begleitdatei oder einer Tool-Voreinstellung übernommen.
{
"title": "My command-line upload",
"description": "A completed local video.\nUploaded privately for review.",
"tags": ["demo", "command-line"],
"madeForKids": false
}
Dateiuploader speichern und ausführen
Speichern Sie das vollständige Programm als youtube-cli/upload.py. Es liest die Datei,
ohne sie oder die Metadaten-Begleitdatei zu verändern. Bei Netzwerkfehlern nach Beginn des Einfügens
gilt das Ergebnis als ungewiss. Das Programm gibt eine zurückgegebene ID auf stderr aus, bevor es
die Antwortmetadaten prüft, damit Sie eine Abweichung untersuchen können. Vermeiden Sie geteilte
Terminalaufzeichnungen: Autorisierungs-URL und Callback sind Teil einer privaten Anmeldung.
import argparse
import contextlib
import json
import logging
import os
import re
import signal
import stat
import sys
from urllib.parse import urlsplit
import httplib2
import requests
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseUpload
from requests.adapters import HTTPAdapter
UPLOAD_SCOPE = 'https://www.googleapis.com/auth/youtube.upload'
MAX_BYTES = 64 * 1024 * 1024
class UserError(Exception):
pass
def stop_at_deadline(*_):
raise UserError('The five-minute deadline expired')
class UploadTransport:
def __init__(self, credentials):
self.session = requests.Session()
self.session.trust_env = False
self.session.mount('https://', HTTPAdapter(max_retries=0))
credentials.apply(self.session.headers)
def request(self, uri, method='GET', body=None, headers=None, **_):
target = urlsplit(uri)
if (target.scheme != 'https' or target.hostname not in
('www.googleapis.com', 'youtube.googleapis.com') or
target.username is not None or target.port not in (None, 443)):
raise UserError('Unexpected upload destination')
response = self.session.request(
method, uri, data=body, headers=headers,
timeout=(10, 30), allow_redirects=False)
if 300 <= response.status_code < 400 and response.status_code != 308:
raise UserError('Upload redirect refused')
return (httplib2.Response(dict(response.headers,
status=str(response.status_code))),
response.content)
def close(self):
self.session.close()
class TimedFlow(InstalledAppFlow):
def fetch_token(self, **kwargs):
return super().fetch_token(timeout=(10, 30), **kwargs)
def authorize(config):
installed = config.get('installed', {})
if (installed.get('auth_uri') != 'https://accounts.google.com/o/oauth2/auth' or
installed.get('token_uri') != 'https://oauth2.googleapis.com/token'):
raise UserError('Use the downloaded Desktop client JSON')
flow = TimedFlow.from_client_config(
config, scopes=[UPLOAD_SCOPE], autogenerate_code_verifier=True)
flow.oauth2session.trust_env = False
flow.oauth2session.mount('https://', HTTPAdapter(max_retries=0))
def refuse_token_redirect(url, headers, request_kwargs):
return url, headers, dict(request_kwargs, allow_redirects=False)
flow.oauth2session.register_compliance_hook('access_token_request', refuse_token_redirect)
try:
if flow.oauth2session.token:
raise UserError('Expected a fresh authorization flow')
with contextlib.redirect_stdout(sys.stderr):
credentials = flow.run_local_server(
host='127.0.0.1', port=0, open_browser=False, timeout_seconds=180,
prompt='consent select_account', include_granted_scopes='false',
access_type='online')
with requests.Session() as check:
check.trust_env = False
response = check.get('https://oauth2.googleapis.com/tokeninfo',
params={'access_token': credentials.token},
timeout=(10, 30), allow_redirects=False)
if response.status_code != 200:
raise UserError('Could not check the new grant')
info = response.json()
if (info.get('aud') != installed['client_id'] or
set(info.get('scope', '').split()) != {UPLOAD_SCOPE} or
int(info.get('expires_in', 0)) < 600):
raise UserError('The new grant must be upload-only and unexpired')
return credentials
finally:
flow.oauth2session.close()
def read_metadata(filename):
with open(filename, encoding='utf-8') as source:
metadata = json.load(source)
if not isinstance(metadata, dict) or set(metadata) != {
'title', 'description', 'tags', 'madeForKids'}:
raise UserError('Metadata needs title, description, tags, and madeForKids only')
title, description, tags = (metadata[key] for key in ('title', 'description', 'tags'))
if not isinstance(title, str) or not title.strip() or len(title) > 100:
raise UserError('Title must contain 1–100 characters')
if not isinstance(description, str) or not isinstance(tags, list) or not all(
isinstance(tag, str) for tag in tags):
raise UserError('Description must be text and tags must be a list of strings')
if type(metadata['madeForKids']) is not bool:
raise UserError('madeForKids must be a JSON boolean')
return {'snippet': {'title': title, 'description': description, 'tags': tags},
'status': {'privacyStatus': 'private',
'selfDeclaredMadeForKids': metadata['madeForKids']}}
def main():
parser = argparse.ArgumentParser(description='Upload one private MP4; no automatic retry')
parser.add_argument('--file', required=True)
parser.add_argument('--metadata', required=True)
parser.add_argument('--channel-id', required=True)
parser.add_argument('--client-secrets', required=True,
help='Desktop JSON filename, or - for one private JSON line on stdin')
args = parser.parse_args()
if not re.fullmatch(r'UC[A-Za-z0-9_-]{22}', args.channel_id):
raise UserError('Expected a UC channel ID')
for filename in (args.file, args.metadata, args.client_secrets):
os.fsencode(filename).decode('utf-8', errors='strict')
body = read_metadata(args.metadata)
with open(args.file, 'rb') as media:
details = os.fstat(media.fileno())
if (not stat.S_ISREG(details.st_mode) or not 0 < details.st_size <= MAX_BYTES or
not args.file.lower().endswith('.mp4')):
raise UserError('Use a completed, nonempty MP4 of at most 64 MiB')
if args.client_secrets == '-':
config = json.loads(sys.stdin.readline())
else:
with open(args.client_secrets, encoding='utf-8') as source:
config = json.load(source)
credentials = authorize(config)
print(f'Confirm the channel selected in your browser: {args.channel_id}', file=sys.stderr)
print('Type that channel ID to insert the private video:', file=sys.stderr, flush=True)
if sys.stdin.readline().strip() != args.channel_id:
raise UserError('Channel confirmation did not match; no insert attempted')
transport = UploadTransport(credentials)
try:
youtube = build('youtube', 'v3', http=transport,
static_discovery=True, cache_discovery=False)
request = youtube.videos().insert(
part='snippet,status', body=body, notifySubscribers=False,
media_body=MediaIoBaseUpload(media, mimetype='video/mp4',
chunksize=8 * 1024 * 1024, resumable=True))
print('Insert starting; inspect the channel before retrying any failure.',
file=sys.stderr, flush=True)
result = None
while result is None:
_, result = request.next_chunk(num_retries=0)
video_id = result.get('id')
if not isinstance(video_id, str) or not re.fullmatch(r'[A-Za-z0-9_-]{11}', video_id):
raise UserError('YouTube did not return a native video ID')
print(f'Returned video ID: {video_id}', file=sys.stderr, flush=True)
if (result.get('snippet', {}).get('channelId') != args.channel_id or
result.get('snippet', {}).get('title') != body['snippet']['title'] or
result.get('status', {}).get('privacyStatus') != 'private'):
raise UserError('Returned metadata did not match; inspect the returned ID')
print(json.dumps({'id': video_id, 'channelId': args.channel_id,
'status': 'accepted_processing_unchecked'}), flush=True)
finally:
transport.close()
if __name__ == '__main__':
logging.disable(logging.CRITICAL)
signal.signal(signal.SIGALRM, stop_at_deadline)
signal.alarm(300)
try:
main()
except (Exception, KeyboardInterrupt) as error:
if isinstance(error, UserError):
print(str(error), file=sys.stderr)
print('Upload not confirmed. If insertion started, the video may exist. '
'Inspect your channel in YouTube Studio before another upload. '
'Check local files, metadata, authorization, and quota.', file=sys.stderr)
sys.exit(1)
finally:
signal.alarm(0)
Legen Sie Ihre fertiggestellte Datei unter ./video.mp4 ab und ersetzen Sie
YOUR_CHANNEL_ID durch die notierte Kanal-ID. Führen Sie dies im selben
übergeordneten Verzeichnis wie den Installationsbefehl aus:
youtube-cli/.venv/bin/python youtube-cli/upload.py \
--file './video.mp4' \
--metadata youtube-cli/metadata.json \
--client-secrets youtube-cli/client_secrets.json \
--channel-id YOUR_CHANNEL_ID
Öffnen Sie den ausgegebenen Autorisierungslink in Ihrem lokalen Browser. Schließen Sie die
Einwilligung innerhalb von drei Minuten ab und geben Sie dann die ID des ausgewählten Kanals an
der Terminal-Eingabeaufforderung ein. Bei Erfolg enthält stdout ausschließlich JSON mit einer
nativen id, der passenden channelId und
"status": "accepted_processing_unchecked". Ein Exit-Code ungleich null, eine fehlende ID oder abweichende
Metadaten gelten als Fehler. Sichern Sie die ID, bevor Sie Ihr Terminal verlassen; das Programm
schreibt keinen lokalen Upload-Beleg.
Weitere Dateien hochladen
Führen Sie für mehrere Videos denselben Befehl jeweils einmal pro explizit angegebener Datei und
passender Metadatendatei aus. Setzen Sie Pfade in Anführungszeichen, auch bei Leerzeichen und
Shell-Sonderzeichen. Stellen Sie Dateinamen mit führendem Bindestrich
./ voran. Das Programm sucht nicht per Glob nach Dateien und leitet
keine Titel aus Dateinamen ab. Jeder Aufruf erfordert eine neue Browser-Einwilligung und
Kanalbestätigung. Halten Sie beim ersten Fehler an und untersuchen Sie das Ergebnis, bevor Sie
einen weiteren Einfügevorgang starten.
Automatisierte Upload-Integration
Ein erzeugendes Programm kann diese CLI aufrufen, nachdem es eine fertiggestellte MP4-Datei geschlossen hat. Die Autorisierung bleibt jedoch interaktiv. Dies ist kein unbeaufsichtigter Scheduler. Das Aufbewahren von Refresh-Tokens und ein dauerhaftes Verzeichnis von Aufgaben und IDs würden ein separates Konzept erfordern. Führen Sie diesen Befehl insbesondere nicht in einer Retry-Schleife aus: Geht die abschließende Antwort verloren, kann ein Video auf YouTube verbleiben, obwohl der Befehl einen Fehler meldet. Dieses Beispiel speichert weder die URL einer fortsetzbaren Sitzung dauerhaft noch stellt es sie nach Prozessende wieder her.
Häufige Probleme beheben
Übertragungslimits
Die Obergrenze von 64 MiB gilt für dieses Beispiel, nicht als Plattformmaximum von YouTube. Die Requests-Transportschicht verwendet für jede Anfrage ein Verbindungstimeout von zehn Sekunden und ein Lesetimeout von 30 Sekunden. Ein separater Prozessalarm von fünf Minuten umfasst Einwilligung und Übertragung. Eine Bandbreitenbegrenzung gibt es nicht. Tritt nach Beginn des Einfügens ein Timeout auf, prüfen Sie den Kanal, statt den Timeout als Beweis dafür zu werten, dass nichts hochgeladen wurde.
Authentifizierungsprobleme
Verwenden Sie Desktop-Zugangsdaten und den Browser auf dem Rechner, auf dem der Befehl läuft, damit
der Loopback-Callback den Listener erreichen kann. Wird die Einwilligung verweigert, prüfen Sie
Zielgruppe, Testnutzerzugriff und aktivierte API der App. Die CLI verwendet bewusst keinen
Token-Cache erneut. Ein gewährter Scope, der nicht exakt youtube.upload entspricht,
wird abgelehnt. Nutzen Sie für diesen eng begrenzten Ablauf einen dedizierten Desktop-Client,
statt die angeforderten Berechtigungen auszuweiten.
Netzwerk- und API-Fehler
Die lokale Validierung erfolgt vor der Einwilligung. Prüfen Sie zuerst Dateipfad und Metadatentypen. Nach der Meldung zum Beginn des Einfügens erfordert ein Netzwerk- oder API-Fehler eine Untersuchung in YouTube Studio des ausgewählten Kanals. Falls eine zurückgegebene ID ausgegeben wurde, verwenden Sie genau diese ID. Ein Verarbeitungsfehler ist von der Annahme der Übertragung zu unterscheiden; die API beschreibt diese Zustände in der Video-Ressource. Beheben Sie die Ursache, bevor Sie entscheiden, ob Sie einen neuen Upload starten.
