angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Eine Liste von YouTube Videos einer Person erhalten

Der YouTube API ist der beste Weg, um eine Liste von YouTube -Videos einer Person zu erhalten, aber er kann viel kosten.

7 September 2023
post main image
https://pixabay.com/users/27707-27707

Vor ein paar Tagen bekam ich die Frage: Kann man alle öffentlichen YouTube Videos einer Person herunterladen, die zwischen 2020 und heute hochgeladen wurden. Die Gesamtzahl der Videos belief sich auf etwa zweihundert. Und nein, ich konnte keinen Zugriff auf das YouTube -Konto dieser Person erhalten.

In diesem Beitrag verwende ich YouTube API , um die erforderlichen Metadaten aus den Videos herunterzuladen, ein Element pro Video. Ich habe in PyPI nachgeschaut, konnte aber kein geeignetes Paket für dieses triviale Problem finden, also habe ich beschlossen, selbst etwas Code zu schreiben. Sie finden den Code unten.
Alles, was er tut, ist, Daten aus YouTube API abzurufen und sie in einer "Item-Datei" zu speichern. Das ist alles. Sie können diese Datei verwenden, um z. B. eine Datei mit Zeilen zu erstellen, bei der jede Zeile einen yt-dlp -Befehl enthält, der das Video herunterlädt. Aber das bleibt Ihnen überlassen.

Wie immer mache ich dies auf Ubuntu 22.04.

yt-dlp und yt-dlp-gui

yt-dlp ist ein Kommandozeilenprogramm, das zum Herunterladen von Dateien aus vielen Quellen verwendet werden kann, einschließlich YouTube. Wir können dieses Programm installieren und dann auch yt-dlp-gui installieren, was uns GUI liefert.

Auf diese Weise habe ich eine Reihe von Dateien heruntergeladen. Gehen Sie zu YouTube, kopieren Sie die Links und fügen Sie sie in yt-dlp-gui ein. Wir wollen aber nicht 200 Video-URLs kopieren und einfügen, was das Gegenteil von DRY (Don't Repeat Yourself) ist!

YouTube API

Um den Download zu automatisieren, müssen wir die Metadaten aller Videodateien der Person abrufen. Diese können wir mit dem YouTube API abrufen. Wenn man sich diese API ansieht, scheint die Methode "YouTube - Data API - Search" der richtige Weg zu sein, siehe Links unten.

Einen YouTube API -Schlüssel erhalten

Um den YouTube API zu verwenden, benötigen Sie einen YouTube API -Schlüssel. Ich werde Sie hier nicht damit langweilen. Es gibt viele Anleitungen im Internet, wie man diesen API Schlüssel bekommt.

Die Person hat keinen YouTube -Kanal?

Um die Suchmethode YouTube API zu verwenden, benötigen wir den Schlüssel channelId, der die Kennung des YouTube -Channels der Person ist. Was aber, wenn die Person keinen Kanal erstellt hat? Dann gibt es immer noch einen channelId. Eine Möglichkeit, die channelId für ein Konto zu finden, ist eine Suche im Internet:

youtube channel <name>

Sie erhalten dann einen Link, der die channelId enthält.

Die Suchmethode YouTube API

Ich muss die Metadaten für etwa zweihundert Videos über einen Zeitraum von drei Jahren abrufen. Die Anzahl der von der Suchmethode YouTube API zurückgegebenen Elemente ist jedoch auf 50 begrenzt, was in der Dokumentation nicht weiter erläutert wird. Glücklicherweise erlaubt die Suchmethode YouTube API eine Suche zwischen den Daten:

  • published_after
  • published_before

Ich beschloss, die Suche in monatliche Suchen aufzuteilen und dann zu hoffen, dass die Person nicht mehr als - irgendeine Grenze - Videos pro Monat hochgeladen hat.

Die YouTube API gibt nicht alle Elemente auf einmal zurück, sondern verwendet Paging. Die Antwort enthält einen Parameter "nextPageToken", wenn es weitere Elemente gibt. In diesem Fall fügen wir diesen zu unserer nächsten Anfrage hinzu, erhalten die Antwort usw., bis dieser Parameter NULL lautet.

Hier ist ein Beispiel für eine Antwort:

{
    "kind": "youtube#searchListResponse",
    "etag": "Hlc-6V55ICoxEujG5nA274peA0o",
    "nextPageToken": "CAUQAA",
    "regionCode": "NL",
    "pageInfo": {
        "totalResults": 29,
        "resultsPerPage": 5
    },
    "items": [
        ...
    ]
}

Und die Elemente sehen so aus:

[
    {
        'kind': 'youtube#searchResult', 
        'etag': <etag>, 
        'id': {
            'kind': 'youtube#video', 
            'videoId': <video id>
        }, 
        'snippet': {
            'publishedAt': '2023-07-12T01:55:21Z', 
            'channelId': <channel id>, 
            'title': <title>,
            'description': <description>, 
            'thumbnails': {
                ...
            },
            'channelTitle': <channel title>, 
            ...
        }
    },
    ...
]

Dateiname Komplikationen

Dies hat nichts mit den aus YouTube API abgerufenen Daten zu tun. Ich bin darauf gestoßen, als ich diese Daten zum Herunterladen von Videodateien mit yt-dlp verwendet habe, und wollte dies mit Ihnen teilen.

Ein typischer yt-dlp -Befehl zum Herunterladen eines YouTube -Videos in einen mp4 lautet:

yt-dlp -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" https://www.youtube.com/watch?v=lRlbcxXnMpQ -o "%(title)s.%(ext)s"

Hier lassen wir yt-dlp den Dateinamen basierend auf dem Titel des Videos erstellen. Je nach Betriebssystem sind jedoch nicht alle Zeichen in einem Dateinamen erlaubt.

Damit er dem Videotitel so ähnlich wie möglich ist, wandelt yt-dlp bestimmte Zeichen in Unicode um, so dass der Dateiname fast wie der Titel des Videos aussieht, aber oft ist er NICHT derselbe! Dies ist eine nette Funktion, aber völlig unbrauchbar, wenn Sie:

  • Dateinamen vergleichen, oder
  • Dateien zwischen verschiedenen Systemen übertragen

Schließlich habe ich mich entschieden, die Dateinamen selbst zu erstellen, indem ich unerwünschte Zeichen durch einen Unterstrich ersetzte. Außerdem habe ich eine Textdatei erstellt, die Zeilen enthält:

<filename> <video title>

Auf diese Weise ist es möglich, den Titel eines Dateinamens ausgehend von einem Dateinamen zu rekonstruieren. Beachten Sie, dass es noch besser ist, einen eindeutigen Wert in den Dateinamen aufzunehmen, wie z. B. das Veröffentlichungsdatum und die Uhrzeit, um Namenskonflikte zu vermeiden.

YouTube API Abspann: beendet ... :-(

Wenn Sie anfangen, diese APIs zu benutzen, erhalten Sie kostenlose Credits von Google , damit Sie loslegen können. Viele Leute im Internet haben bereits davor gewarnt, dass die YouTube API Suchmethode eine Menge Credits verbraucht.

Ich habe einige kurze Experimente und drei vollständige Durchläufe durchgeführt. Während des dritten vollständigen Durchlaufs war mein Guthaben aufgebraucht. Das ist schnell! Oder, das ist eine Menge Geld oder wenige einfache Durchläufe!

Jedenfalls hatte ich beim zweiten Durchlauf schon alle Daten gesammelt, die ich wollte, und das reichte für dieses Projekt. Und keine Panik, die kostenlosen Credits werden jeden Tag zurückgesetzt.

Der Code

Wenn Sie es selbst ausprobieren wollen, hier ist der Code, geben Sie den channelId der Person und Ihren YouTube API Schlüssel ein. Wir beginnen mit dem letzten Monat, rufen Elemente ab, speichern Elemente und gehen zum vorhergehenden Monat über, bis wir beim letzten Monat angelangt sind. Wir geben Anfang und Ende als Tuples an.
Die "Artikeldatei" wird mit den aus YouTube API abgerufenen Artikeln aus JSON geladen. Positionen werden nur für neue videoIds hinzugefügt. Das bedeutet, dass Sie diese Datei zwischen den Läufen nicht löschen müssen.

Installieren Sie diese zuerst:

pip install python-dateutil
pip install requests

Den Code:

# get_video_list.py
import calendar
import datetime
import json
import logging
import os
import sys
import time
import urllib.parse

from dateutil import parser
from dateutil.relativedelta import relativedelta 
import requests

def get_logger(
    console_log_level=logging.DEBUG,
    file_log_level=logging.DEBUG,
    log_file=os.path.splitext(__file__)[0] + '.log',
):
    logger_format = '%(asctime)s %(levelname)s [%(filename)-30s%(funcName)30s():%(lineno)03s] %(message)s'
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)
    if console_log_level:
        # console
        console_handler = logging.StreamHandler(sys.stdout)
        console_handler.setLevel(console_log_level)
        console_handler.setFormatter(logging.Formatter(logger_format))
        logger.addHandler(console_handler)
    if file_log_level:
        # file
        file_handler = logging.FileHandler(log_file)
        file_handler.setLevel(file_log_level)
        file_handler.setFormatter(logging.Formatter(logger_format))
        logger.addHandler(file_handler)
    return logger

logger = get_logger()


class YouTubeUtils:
    def __init__(
        self,
        logger=None,
        channel_id=None,
        api_key=None,
        yyyy_mm_start=None,
        yyyy_mm_last=None,
        items_file=None,
    ):
        self.logger = logger
        self.channel_id = channel_id
        self.api_key = api_key
        self.items_file = items_file

        # create empty items file if not exists
        if not os.path.exists(self.items_file):
            items = []
            json_data = json.dumps(items)
            with open(self.items_file, 'w') as fo:
                fo.write(json_data)

        self.year_month_dt_start = datetime.datetime(yyyy_mm_start[0], yyyy_mm_start[1], 1, 0, 0, 0)
        self.year_month_dt_current = self.year_month_dt_start
        self.year_month_dt_last = datetime.datetime(yyyy_mm_last[0], yyyy_mm_last[1], 1, 0, 0, 0)

        # api request
        self.request_delay = 3
        self.request_timeout = 6

    def get_previous_year_month(self):
        self.logger.debug(f'()')
        self.year_month_dt_current -= relativedelta(months=1)
        self.logger.debug(f'year_month_dt_current = {self.year_month_dt_current}')
        if self.year_month_dt_current < self.year_month_dt_last:
            return None, None
        yyyy = self.year_month_dt_current.year
        m = self.year_month_dt_current.month
        return yyyy, m

    def get_published_between(self, yyyy, m):
        last_day = calendar.monthrange(yyyy, m)[1]
        published_after = f'{yyyy}-{m:02}-01T00:00:00Z'
        published_before = f'{yyyy}-{m:02}-{last_day:02}T23:59:59Z'
        self.logger.debug(f'published_after = {published_after}, published_before = {published_before}')
        return published_after, published_before

    def get_data_from_youtube_api(self, url):
        self.logger.debug(f'(url = {url})')
        r = None
        try:
            r = requests.get(url, timeout=self.request_timeout)
        except Exception as e:
            self.logger.exception(f'url = {url}')
            raise
        self.logger.debug(f'status_code = {r.status_code}')
        if r.status_code != 200:
            raise Exception(f'url = {url}, status_code = {r.status_code}, r = {r.__dict__}')
        try:
            data = r.json()
            self.logger.debug(f'data = {data}')
        except Exception as e:
            raise Exception(f'url = {url}, converting json, status_code = {r.status_code}, r = {r.__dict__}')
            raise
        return data

    def add_items_to_items_file(self, items_to_add):
        self.logger.debug(f'(items_to_add = {items_to_add})')
        # read file + json to dict
        with open(self.items_file, 'r') as fo:
            json_data = fo.read()
        items = json.loads(json_data)
        self.logger.debug(f'items = {items}')
        # add only unique video_ids
        video_ids = []
        for item in items:
            id = item.get('id')
            if id is None:
                continue
            video_id = id.get('videoId')
            if video_id is None:
                continue
            video_ids.append(video_id)
        self.logger.debug(f'video_ids = {video_ids}')
        items_added_count = 0
        for item_to_add in items_to_add:
            self.logger.debug(f'item_to_add = {item_to_add})')
            kind_to_add = item_to_add['id']['kind']
            if kind_to_add != 'youtube#video':
                self.logger.debug(f'skipping kind_to_add = {kind_to_add})')
                continue
            video_id_to_add = item_to_add['id']['videoId']
            if video_id_to_add not in video_ids:
                self.logger.debug(f'adding video_id_to_add = {video_id_to_add})')
                items.append(item_to_add)
                items_added_count += 1
                video_ids.append(video_id_to_add)
        self.logger.debug(f'items_added_count = {items_added_count})')
        if items_added_count > 0:
            # dict to json + write file
            json_data = json.dumps(items)
            with open(self.items_file, 'w') as fo:
                fo.write(json_data)
        return items_added_count

    def fetch_year_month_videos(self, yyyy, m):
        self.logger.debug(f'(yyyy = {yyyy}, m = {m})')
        published_after, published_before = self.get_published_between(yyyy, m)
        url_base = 'https://youtube.googleapis.com/youtube/v3/search?'
        url_params = {
            'part': 'snippet,id',
            'channelId': self.channel_id,
            'publishedAfter': published_after,
            'publishedBefore': published_before,
            'sort': 'date',
            'key': self.api_key,
        }
        url = url_base + urllib.parse.urlencode(url_params)

        total_items_added_count = 0
        while True:
            time.sleep(self.request_delay)
            data = self.get_data_from_youtube_api(url)
            page_info = data.get('pageInfo')
            self.logger.debug(f'page_info = {page_info})')

            items = data.get('items')
            if items is None:
                break
            if not isinstance(items, list) or len(items) == 0:
                break
            # add items
            total_items_added_count += self.add_items_to_items_file(items)

            next_page_token = data.get('nextPageToken')
            self.logger.debug(f'next_page_token = {next_page_token})')
            if next_page_token is None:
                break
            # add next page token
            url_params['pageToken'] = next_page_token
            url = url_base + urllib.parse.urlencode(url_params)

        self.logger.debug(f'total_items_added_count = {total_items_added_count})')
        return total_items_added_count


def main():
    # replace CHANNEL_ID and API_KEY with your values
    yt_utils = YouTubeUtils(
        logger=logger,
        channel_id='CHANNEL_ID',
        api_key='API_KEY',
        # current month + 1
        yyyy_mm_start=(2023, 10),
        yyyy_mm_last=(2020, 1),
        items_file='./items.json',
    )

    while True:
        yyyy, m = yt_utils.get_previous_year_month()
        if yyyy is None or m is None:
            break
        logger.debug(f'fetching for {yyyy}-{m:02}')
        yt_utils.fetch_year_month_videos(yyyy, m)
        

if __name__ == '__main__':
    main() 

Zusammenfassung

Das Abrufen von Daten über die YouTube -Videos mithilfe der YouTube API ist nicht sehr schwierig. Wir haben eine separate "Item-Datei" erstellt, die wir für die weitere Verarbeitung verwenden.

Das war ein lustiges Projekt mit einer großen Überraschung, als ich anfing, YouTube -Videos mit Hilfe der Informationen in der "Artikeldatei" herunterzuladen. yt-dlp kann Dateinamen erzeugen, die dem Titel des Videos sehr ähnlich sind. Dies geschieht durch das Einfügen von Unicode-Zeichen und sieht toll aus, aber ich fand das sehr verwirrend.

Oh, und noch eine Überraschung. Die Verwendung des YouTube API kann sehr teuer sein. Es brauchte nicht viel, um die täglichen kostenlosen Credits aufzubrauchen.

Links / Impressum

YouTube - Data API - Search
https://developers.google.com/youtube/v3/docs/search

yt-dlp
https://github.com/yt-dlp/yt-dlp

yt-dlp-gui and others
https://www.reddit.com/r/youtubedl/wiki/info-guis

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.