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

Een lijst met YouTube video's van een persoon ophalen

De YouTube API is de manier om een lijst met YouTube video's van een persoon te krijgen, maar het kan je veel kosten.

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

Een paar dagen geleden kreeg ik de vraag: Kun je alle openbare YouTube video's van een persoon downloaden, die tussen 2020 en vandaag zijn geüpload. Het totale aantal video's was ongeveer tweehonderd. En nee, ik kon geen toegang krijgen tot het YouTube account van deze persoon.

In deze post gebruik ik YouTube API om de vereiste metadata van de video's te downloaden, één item per video. Ik zocht in PyPI, maar kon geen geschikt pakket vinden voor dit triviale probleem, dus besloot ik zelf wat code te schrijven. Je kunt de code hieronder vinden.
Het enige wat het doet is gegevens ophalen uit de YouTube API en deze opslaan in een 'items-bestand'. Dat is alles. Je kunt dit bestand gebruiken om bijvoorbeeld een bestand met regels te maken waarbij elke regel een yt-dlp commando bevat dat de video downloadt. Maar dat is aan jou.

Zoals altijd doe ik dit op Ubuntu 22.04.

yt-dlp en yt-dlp-gui

yt-dlp is een opdrachtregelprogramma waarmee bestanden van vele bronnen gedownload kunnen worden, waaronder YouTube. We kunnen dit installeren en dan ook yt-dlp-gui installeren, waardoor we GUI krijgen.

Zo heb ik een aantal bestanden gedownload. Ga naar YouTube, kopieer de links en plak ze in yt-dlp-gui. Maar we willen geen 200 video urls kopiëren en plakken, dat is het tegenovergestelde van DRY (Don't Repeat Yourself)!

YouTube API

Om de download te automatiseren, moeten we de metadata van alle videobestanden van de persoon ophalen. We kunnen deze ophalen met de YouTube API. Als we naar deze API kijken, blijkt dat we het beste de methode "YouTube - Data API - Search" kunnen gebruiken, zie onderstaande koppelingen.

Een YouTube API sleutel krijgen

Om de YouTube API te gebruiken heb je een YouTube API sleutel nodig. Daar ga ik je hier niet mee vervelen. Er zijn veel instructies op het internet te vinden hoe je deze API sleutel kunt krijgen.

Heeft de persoon geen YouTube kanaal?

Om de YouTube API zoekmethode te gebruiken, hebben we de channelId nodig, wat de id is van het YouTube kanaal van de persoon. Maar wat als de persoon geen chatroom heeft aangemaakt? Dan is er nog steeds een channelId. Een manier om de channelId voor een account te vinden, is door op het internet te zoeken:

youtube channel <name>

Dit geeft een link met de channelId.

De zoekmethode YouTube API

Ik moet de metagegevens krijgen voor zo'n tweehonderd video's over een periode van drie jaar. Het aantal items dat wordt teruggegeven door de zoekmethode YouTube API is echter beperkt tot 50, de documentatie is hier niet erg over te spreken. Gelukkig kunnen we met de zoekmethode YouTube API tussen datums zoeken:

  • published_after
  • published_before

Ik besloot de zoekopdracht op te splitsen in maandelijkse zoekopdrachten en dan te hopen dat de persoon niet meer dan - een bepaalde limiet - video's per maand uploadde.

De YouTube API retourneert niet alle items tegelijk, maar gebruikt paginering. De respons bevat een parameter 'nextPageToken' als er meer items zijn. In dit geval voegen we dit toe aan ons volgende verzoek, ontvangen we het antwoord, enzovoort totdat deze parameter NULL is.

Hier is een voorbeeld van een antwoord:

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

En de items zien er zo uit:

[
    {
        '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>, 
            ...
        }
    },
    ...
]

Bestandsnaam complicaties

Dit heeft niets te maken met de gegevens die zijn opgehaald uit YouTube API. Ik kwam dit tegen toen ik deze gegevens gebruikte om videobestanden te downloaden met yt-dlp en wilde dit met jullie delen.

Een typisch yt-dlp commando om een YouTube video te downloaden naar een mp4 is:

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 laten we yt-dlp de bestandsnaam maken op basis van de titel van de video. Maar afhankelijk van het besturingssysteem dat je gebruikt, zijn niet alle tekens toegestaan in een bestandsnaam.

Om de bestandsnaam zoveel mogelijk op de titel van de video te laten lijken, converteert yt-dlp bepaalde tekens naar unicode, waardoor de bestandsnaam er bijna hetzelfde uitziet als de titel van de video, maar vaak is het NIET hetzelfde! Dit is een leuke functie, maar totaal onbruikbaar als je:

  • bestandsnamen wilt vergelijken, of
  • bestanden wilt overzetten tussen verschillende systemen

Uiteindelijk heb ik ervoor gekozen om de bestandsnamen zelf te maken door ongewenste tekens te vervangen door een underscore. Daarnaast heb ik een tekstbestand gemaakt met regels:

<filename> <video title>

Op deze manier is het mogelijk om de titel van een bestandsnaam te reconstrueren op basis van een bestandsnaam. Merk op dat het nog beter is om een unieke waarde in de bestandsnaam op te nemen, zoals de gepubliceerde datum en tijd, om naamsclashes te voorkomen.

YouTube API credits: klaar ... :-(

Als je deze APIs gaat gebruiken, krijg je gratis credits van Google zodat je aan de slag kunt. Veel mensen op internet waarschuwden al dat de zoekmethode van YouTube API veel credits kostte.

Ik heb een aantal korte experimenten gedaan en drie volledige runs. Tijdens de derde volledige run waren mijn credits op. Dat is snel! Of, dat is veel geld of weinig eenvoudige runs!

Hoe dan ook, tijdens de tweede run had ik al de gegevens verzameld die ik wilde, dus dat was genoeg voor dit project. En geen paniek, de gratis credits worden elke dag gereset.

De code

Als je het zelf wilt proberen, hier is de code, voer de channelId van de persoon in en jouw YouTube API sleutel. We beginnen bij de meest recente maand, halen items op, slaan items op en gaan naar de vorige maand tot we bij de laatste maand zijn. We specificeren begin en laatste als Tuples.
Het 'items bestand' wordt geladen met de JSON items opgehaald uit de YouTube API. Er worden alleen items toegevoegd voor nieuwe videoIds. Dit betekent dat het niet nodig is om dit bestand te verwijderen tussen runs.

Installeer deze eerst:

pip install python-dateutil
pip install requests

De 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() 

Samenvatting

Het ophalen van gegevens over de YouTube video's met behulp van de YouTube API is niet erg moeilijk. We hebben een apart 'items-bestand' gemaakt om te gebruiken voor verdere verwerking.

Dit was een leuk project met een grote verrassing toen ik eenmaal begon met het downloaden van YouTube video's met behulp van de informatie in het 'items bestand'. yt-dlp kan bestandsnamen genereren die heel dicht bij de titel van de video liggen. Het doet dit door unicode karakters in te voegen en het ziet er geweldig uit, maar ik vond dit erg verwarrend.

Oh, en nog een verrassing. Het gebruik van de YouTube API kan erg duur zijn. Er was niet veel voor nodig om de dagelijkse gratis credits op te gebruiken.

Links / credits

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.