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

Obtener una lista de YouTube vídeos de una persona

El YouTube API es la forma de conseguir una lista de vídeos YouTube de una persona, pero puede costarte mucho.

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

Hace unos días recibí la pregunta ¿Se puede descargar todos los videos YouTube pública de una persona, que fueron subidos entre 2020 y hoy. El número total de vídeos era de unos doscientos. Y no, no he podido acceder a la cuenta YouTube de esta persona.

En este post, utilizo la YouTube API para descargar los metadatos necesarios de los vídeos, un elemento por vídeo. Busqué en PyPI, pero no pude encontrar un paquete adecuado para este problema trivial, así que decidí escribir algo de código yo mismo. Usted puede encontrar el código de abajo.
Todo lo que hace es recuperar datos del YouTube API y almacenarlos en un 'fichero de items'. Eso es todo. Puedes usar este archivo para crear, por ejemplo, un archivo con líneas donde cada línea contenga un comando yt-dlp que descargue el vídeo. Pero eso es cosa tuya.

Como siempre estoy haciendo esto en Ubuntu 22.04.

yt-dlp y yt-dlp-gui

yt-dlp es un programa de línea de comandos que se puede utilizar para descargar archivos de muchas fuentes, incluyendo YouTube. Podemos instalar esto y luego también instalar yt-dlp-gui, que nos da un GUI.

Así es como he descargado una serie de archivos. Vamos a YouTube, copiamos los enlaces y los pegamos en yt-dlp-gui. Pero no queremos copiar-pegar 200 urls de vídeo, ¡que es lo contrario de DRY (No te repitas)!

YouTube API

Para automatizar la descarga, necesitamos recuperar los metadatos de todos los archivos de vídeo de la persona. Podemos recuperarlos utilizando el YouTube API. Mirando este API, el camino a seguir parece ser el método "YouTube - Data API - Search", ver enlaces más abajo.

Obtener una clave YouTube API

Para utilizar el YouTube API necesitas una clave YouTube API . No voy a aburrirte con esto aquí. Hay muchas instrucciones en Internet sobre cómo obtener esta clave API .

¿La persona no tiene YouTube canal?

Para utilizar el método de búsqueda YouTube API , necesitamos el channelId, que es el id del canal YouTube de la persona. Pero, ¿y si la persona no ha creado ningún canal? Entonces sigue habiendo un channelId. Una forma de encontrar el channelId para una cuenta es buscar en Internet:

youtube channel <name>

Esto dará un enlace que contiene el channelId.

El método de búsqueda YouTube API

Debo obtener los metadatos de unos doscientos vídeos durante un periodo de tres años. Sin embargo, el número de elementos devueltos por el método de búsqueda YouTube API está limitado a 50, la documentación no es muy al respecto. Afortunadamente el método de búsqueda YouTube API nos permite buscar entre fechas:

  • published_after
  • published_before

Decidí dividir la búsqueda en búsquedas mensuales y así esperar que la persona no subiera más de -algún límite- vídeos al mes.

El YouTube API no devuelve todos los elementos a la vez, sino que utiliza la paginación. La respuesta contiene un parámetro 'nextPageToken' si hay más elementos. En este caso, lo añadimos a nuestra siguiente petición, recibimos la respuesta, etc. hasta que este parámetro sea NULL.

He aquí un ejemplo de respuesta:

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

Y los elementos tienen este aspecto

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

Complicaciones de archivo

Esto no tiene nada que ver con los datos recuperados del YouTube API. Me encontré con esto cuando se utilizan estos datos para descargar archivos de vídeo con yt-dlp y quería compartir esto con ustedes.

Un comando típico yt-dlp para descargar un video YouTube en un mp4 es:

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"

Aquí dejamos que yt-dlp cree el nombre del archivo basándose en el título del vídeo. Pero dependiendo del sistema operativo que utilices, no todos los caracteres están permitidos en un nombre de archivo.

Para que se parezca lo más posible al título del vídeo, yt-dlp convierte ciertos caracteres a unicode, lo que hace que el nombre del archivo se parezca casi al título del vídeo, ¡pero a menudo NO es el mismo! Esta es una buena característica, pero totalmente inutilizable si quieres

  • comparar nombres de archivos, o
  • transferir archivos entre diferentes sistemas

Al final, opté por crear los propios nombres de archivo sustituyendo los caracteres no deseados por un guión bajo. Además, creé un archivo de texto con líneas que contenían:

<filename> <video title>

De este modo, es posible reconstruir el título de un fichero partiendo de un nombre de fichero. Tenga en cuenta que es incluso mejor incluir un valor único en el nombre del archivo, como la fecha y hora de publicación, para evitar conflictos de nombres.

YouTube API créditos: terminado ... :-(

Cuando empiezas a usar estos APIs, obtienes créditos gratis de Google para que puedas empezar. Mucha gente en Internet ya advirtió que el método de búsqueda YouTube API consumía muchos créditos.

Hice algunos experimentos cortos y tres ejecuciones completas. Durante la tercera ejecución completa, se me acabaron los créditos. ¡Qué rápido! O, ¡eso es mucho dinero o pocas ejecuciones simples!

En cualquier caso, durante la segunda ejecución ya había recogido todos los datos que quería, así que fue suficiente para este proyecto. Y que no cunda el pánico, los créditos gratuitos se reinician cada día.

El código

Si quieres probarlo tú mismo, aquí tienes el código, introduce el channelId de la persona y tu clave YouTube API . Empezamos por el mes más reciente, recuperamos elementos, guardamos elementos y pasamos al mes anterior hasta llegar al último mes. Especificamos inicio y último como Tuples.
El 'fichero de artículos' se carga con los artículos JSON recuperados del YouTube API. Sólo se añaden elementos para los nuevos videoIds. Esto significa que no hay necesidad de eliminar este archivo entre ejecuciones.

Instale estos primero:

pip install python-dateutil
pip install requests

El código:

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

Resumen

Recuperar datos sobre los vídeos YouTube utilizando el YouTube API no es muy difícil. Creamos un "archivo de elementos" independiente para utilizarlo en el procesamiento posterior.

Fue un proyecto divertido con una gran sorpresa cuando empecé a descargar vídeos YouTube utilizando la información del "archivo de elementos". yt-dlp puede generar nombres de archivo muy parecidos al título del vídeo. Lo hace insertando caracteres unicode y queda muy bien, pero me ha resultado muy confuso.

Ah, y otra sorpresa. Usar el YouTube API puede ser muy costoso. No me costó mucho agotar los créditos gratuitos diarios.

Enlaces / créditos

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

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.