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.
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
Récent
- Un commutateur de base de données avec HAProxy et HAProxy Runtime API
- Docker Swarm rolling updates
- Masquer les clés primaires de la base de données UUID de votre application web
- Don't Repeat Yourself (DRY) avec Jinja2
- SQLAlchemy, PostgreSQL, nombre maximal de lignes par user
- Afficher les valeurs des filtres dynamiques SQLAlchemy
Les plus consultés
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- Réduire les temps de réponse d'un Flask SQLAlchemy site web
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb