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

Obtenir une liste des vidéos YouTube d'une personne

Le YouTube API est la solution pour obtenir une liste de vidéos YouTube d'une personne, mais il peut être très coûteux.

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

Il y a quelques jours, j'ai reçu la question suivante : Pouvez-vous télécharger toutes les vidéos publiques YouTube d'une personne, qui ont été téléchargées entre 2020 et aujourd'hui. Le nombre total de vidéos était d'environ 200. Et non, je n'ai pas pu accéder au compte YouTube de cette personne.

Dans ce billet, j'utilise le compte YouTube API pour télécharger les métadonnées requises des vidéos, un élément par vidéo. J'ai cherché dans PyPI, mais je n'ai pas trouvé de package approprié pour ce problème trivial, j'ai donc décidé d'écrire du code moi-même. Vous trouverez le code ci-dessous.
Tout ce qu'il fait est de récupérer les données du YouTube API et de les stocker dans un "fichier d'éléments". C'est tout. Vous pouvez utiliser ce fichier pour créer, par exemple, un fichier avec des lignes où chaque ligne contient une commande yt-dlp qui télécharge la vidéo. Mais c'est à vous de voir.

Comme toujours, je fais cela sur Ubuntu 22.04.

yt-dlp et yt-dlp-gui

yt-dlp est un programme en ligne de commande qui peut être utilisé pour télécharger des fichiers depuis de nombreuses sources, y compris YouTube. Nous pouvons l'installer et installer également yt-dlp-gui, ce qui nous donne GUI.

C'est ainsi que j'ai téléchargé un certain nombre de fichiers. Allez dans YouTube, copiez les liens et collez-les dans yt-dlp-gui. Mais nous ne voulons pas copier-coller 200 urls de vidéos, ce qui est contraire à DRY (Don't Repeat Yourself) !

YouTube API

Pour automatiser le téléchargement, nous devons récupérer les métadonnées de tous les fichiers vidéo de la personne. Nous pouvons les récupérer à l'aide de la YouTube API. En examinant cette API, il semble que la méthode "YouTube - Data API - Search" soit la plus appropriée, voir les liens ci-dessous.

Obtenir une clé YouTube API

Pour utiliser la méthode YouTube API , vous avez besoin d'une clé YouTube API . Je ne vais pas vous ennuyer avec cela ici. Il existe de nombreuses instructions sur Internet pour obtenir cette clé API .

La personne n'a pas de canal YouTube ?

Pour utiliser la méthode de recherche YouTube API , nous avons besoin de la clé channelId, qui est l'identifiant du canal YouTube de la personne. Mais que se passe-t-il si la personne n'a pas créé de canal ? Dans ce cas, il existe toujours un channelId. Une façon de trouver la channelId pour un compte est de faire une recherche sur Internet :

youtube channel <name>

Vous obtiendrez un lien contenant le channelId.

La méthode de recherche YouTube API

Je dois obtenir les métadonnées de quelque deux cents vidéos sur une période de trois ans. Cependant, le nombre d'éléments retournés par la méthode de recherche YouTube API est limité à 50, la documentation n'est pas très explicite à ce sujet. Heureusement, la méthode de recherche YouTube API permet d'effectuer des recherches entre les dates :

  • published_after
  • published_before

J'ai décidé de diviser la recherche en recherches mensuelles et d'espérer que la personne n'a pas téléchargé plus de - une certaine limite - vidéos par mois.

La réponse YouTube API ne renvoie pas tous les éléments en même temps, mais utilise la pagination. La réponse contient un paramètre "nextPageToken" s'il y a plus d'éléments. Dans ce cas, nous l'ajoutons à notre prochaine demande, recevons la réponse, etc. jusqu'à ce que ce paramètre soit NULL.

Voici un exemple de réponse :

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

Et les éléments ressemblent à ceci :

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

Nom de fichier complications

Cela n'a rien à voir avec les données extraites de YouTube API. Je l'ai découvert en utilisant ces données pour télécharger des fichiers vidéo avec yt-dlp et je voulais le partager avec vous.

Voici une commande typique de yt-dlp pour télécharger une vidéo YouTube dans un mp4 :

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"

Ici, nous laissons yt-dlp créer le nom de fichier à partir du titre de la vidéo. Mais selon le système d'exploitation que vous utilisez, tous les caractères ne sont pas autorisés dans un nom de fichier.

Pour qu'il ressemble le plus possible au titre de la vidéo, yt-dlp convertit certains caractères en unicode, ce qui fait que le nom de fichier ressemble presque au titre de la vidéo, mais souvent ce n'est PAS la même chose ! Il s'agit d'une fonctionnalité intéressante, mais totalement inutilisable si vous souhaitez :

  • comparer des noms de fichiers, ou
  • transférer des fichiers entre différents systèmes

Finalement, j'ai choisi de créer les noms de fichiers eux-mêmes en remplaçant les caractères indésirables par un trait de soulignement. En outre, j'ai créé un fichier texte dont les lignes contiennent :

<filename> <video title>

De cette manière, il est possible de reconstituer le titre d'un nom de fichier à partir d'un nom de fichier. Notez qu'il est encore mieux d'inclure une valeur unique dans le nom de fichier, comme la date et l'heure de publication, afin d'éviter les conflits de noms.

YouTube API credits : terminé ... :-(

Lorsque vous commencez à utiliser ces APIs, vous recevez des crédits gratuits de Google afin de pouvoir commencer. De nombreuses personnes sur Internet ont déjà signalé que la méthode de recherche YouTube API consommait beaucoup de crédits.

J'ai fait quelques petites expériences et trois essais complets. Au cours de la troisième exécution complète, mes crédits ont été épuisés. C'est rapide ! Ou alors, c'est beaucoup d'argent ou peu d'exécutions simples !

Quoi qu'il en soit, au cours du deuxième essai, j'avais déjà recueilli toutes les données que je voulais, et c'était donc suffisant pour ce projet. Et pas de panique, les crédits gratuits sont réinitialisés tous les jours.

Le code

Si vous voulez essayer vous-même, voici le code, entrez le channelId de la personne et votre clé YouTube API . Nous commençons par le mois le plus récent, nous récupérons les éléments, nous les sauvegardons et nous passons au mois précédent jusqu'à ce que nous arrivions au dernier mois. Nous spécifions le début et la fin comme Tuples.
Le "fichier d'éléments" est chargé avec les éléments JSON extraits du YouTube API. Des éléments sont ajoutés uniquement pour les nouveaux videoIds. Cela signifie qu'il n'est pas nécessaire de supprimer ce fichier entre les exécutions.

Installez-les d'abord :

pip install python-dateutil
pip install requests

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

Résumé

Il n'est pas très difficile d'obtenir des données sur les vidéos YouTube en utilisant le YouTube API . Nous avons créé un "fichier d'éléments" distinct à utiliser pour la suite du traitement.

Ce fut un projet amusant qui m'a réservé une grande surprise lorsque j'ai commencé à télécharger les vidéos YouTube à l'aide des informations contenues dans le "fichier d'éléments". yt-dlp peut générer des noms de fichiers très proches du titre de la vidéo. Pour ce faire, il insère des caractères unicode, ce qui est très joli, mais j'ai trouvé cela très déroutant.

Oh, et une autre surprise. L'utilisation du YouTube API peut être très coûteuse. Il ne m'a pas fallu beaucoup de temps pour épuiser les crédits quotidiens gratuits.

Liens / crédits

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

En savoir plus...

Web automation YouTube

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.