はじめに
Wallabagは素晴らしい「あとで読む」サービスですが、YouTube動画を保存したときにサムネイルが表示されないことがあります。
これはWallabagの記事取得エンジン(graby)がYouTubeの特殊な構造からサムネイルを安定して取得できないためです。特にYouTube Shortsでは顕著です。
本記事では、Dockerで動作する小さなワーカーを追加して、YouTube動画のサムネイルを自動設定する方法を紹介します。
仕組み
YouTubeのサムネイルは以下のURLで確実に取得できます:
https://img.youtube.com/vi/{VIDEO_ID}/maxresdefault.jpg
例えば https://www.youtube.com/watch?v=dQw4w9WgXcQ なら:
https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg
このワーカーは定期的にWallabagのDBをチェックし、サムネイルが未設定のYouTube記事を見つけたら自動でサムネイルURLを設定します。
成功例
好きな音楽をタブで保管していたためよく激重ブラウザ化していた( ・∇・)
前提条件
- Docker + Docker Compose でWallabagを運用している
- PostgreSQL または MySQL/MariaDB をDBに使用している
セットアップ
1. ディレクトリ作成
Wallabagのdocker-compose.ymlがあるディレクトリにytthumbフォルダを作成します。
mkdir ytthumb
2. Pythonスクリプト作成
PostgreSQLの場合
ytthumb/ytthumb.py:
#!/usr/bin/env python3
"""
YouTube Thumbnail Injector for Wallabag (PostgreSQL)
"""
import re
import os
import time
from urllib.parse import unquote
import psycopg2
YOUTUBE_RE = re.compile(
r"(?:youtube\.com/(?:watch\?v(?:=|%3D)|shorts/)|youtu\.be/)([A-Za-z0-9_\-]{11})",
re.IGNORECASE
)
def extract_video_id(url: str) -> str | None:
decoded_url = unquote(url)
match = YOUTUBE_RE.search(decoded_url)
if match:
return match.group(1)
match = YOUTUBE_RE.search(url)
return match.group(1) if match else None
def get_thumbnail_url(video_id: str) -> str:
return f"https://img.youtube.com/vi/{video_id}/maxresdefault.jpg"
def get_db_connection():
return psycopg2.connect(
host=os.environ.get("DB_HOST", "wallabag-db"),
port=os.environ.get("DB_PORT", "5432"),
user=os.environ.get("DB_USER", "wallabag"),
password=os.environ.get("DB_PASS", "wallabagpass"),
database=os.environ.get("DB_NAME", "wallabag"),
)
def update_thumbnails():
conn = get_db_connection()
try:
with conn.cursor() as cur:
cur.execute("""
SELECT id, url FROM wallabag_entry
WHERE (preview_picture IS NULL OR preview_picture = '')
AND (url LIKE '%youtube.com%' OR url LIKE '%youtu.be%')
""")
rows = cur.fetchall()
for entry_id, url in rows:
video_id = extract_video_id(url)
if video_id:
thumb_url = get_thumbnail_url(video_id)
cur.execute(
"UPDATE wallabag_entry SET preview_picture = %s WHERE id = %s",
(thumb_url, entry_id)
)
print(f"[UPDATE] id={entry_id} -> {thumb_url}")
conn.commit()
finally:
conn.close()
def main():
interval = int(os.environ.get("CHECK_INTERVAL", "300"))
print(f"YouTube Thumbnail Injector started (interval: {interval}s)")
while True:
try:
update_thumbnails()
except Exception as e:
print(f"[ERROR] {e}")
time.sleep(interval)
if __name__ == "__main__":
main()
MySQL/MariaDBの場合
ytthumb/ytthumb.py:
#!/usr/bin/env python3
"""
YouTube Thumbnail Injector for Wallabag (MySQL/MariaDB)
"""
import re
import os
import time
from urllib.parse import unquote
import pymysql
YOUTUBE_RE = re.compile(
r"(?:youtube\.com/(?:watch\?v(?:=|%3D)|shorts/)|youtu\.be/)([A-Za-z0-9_\-]{11})",
re.IGNORECASE
)
def extract_video_id(url: str) -> str | None:
decoded_url = unquote(url)
match = YOUTUBE_RE.search(decoded_url)
if match:
return match.group(1)
match = YOUTUBE_RE.search(url)
return match.group(1) if match else None
def get_thumbnail_url(video_id: str) -> str:
return f"https://img.youtube.com/vi/{video_id}/maxresdefault.jpg"
def get_db_connection():
return pymysql.connect(
host=os.environ.get("DB_HOST", "wallabag-db"),
port=int(os.environ.get("DB_PORT", "3306")),
user=os.environ.get("DB_USER", "wallabag"),
password=os.environ.get("DB_PASS", "wallabagpass"),
database=os.environ.get("DB_NAME", "wallabag"),
)
def update_thumbnails():
conn = get_db_connection()
try:
with conn.cursor() as cur:
cur.execute("""
SELECT id, url FROM wallabag_entry
WHERE (preview_picture IS NULL OR preview_picture = '')
AND (url LIKE '%youtube.com%' OR url LIKE '%youtu.be%')
""")
rows = cur.fetchall()
for entry_id, url in rows:
video_id = extract_video_id(url)
if video_id:
thumb_url = get_thumbnail_url(video_id)
cur.execute(
"UPDATE wallabag_entry SET preview_picture=%s WHERE id=%s",
(thumb_url, entry_id)
)
print(f"[UPDATE] id={entry_id} -> {thumb_url}")
conn.commit()
finally:
conn.close()
def main():
interval = int(os.environ.get("CHECK_INTERVAL", "300"))
print(f"YouTube Thumbnail Injector started (interval: {interval}s)")
while True:
try:
update_thumbnails()
except Exception as e:
print(f"[ERROR] {e}")
time.sleep(interval)
if __name__ == "__main__":
main()
3. requirements.txt作成
ytthumb/requirements.txt:
# PostgreSQLの場合
psycopg2-binary
# MySQL/MariaDBの場合は以下に変更
# pymysql
4. Dockerfile作成
ytthumb/Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY ytthumb.py .
CMD ["python", "-u", "ytthumb.py"]
5. docker-compose.ymlに追加
wallabag-ytthumb:
build: ./ytthumb
container_name: wallabag-ytthumb
depends_on:
- wallabag-db # あなたのDB名に合わせて変更
environment:
- DB_HOST=wallabag-db
- DB_PORT=5432 # MySQLなら3306
- DB_USER=wallabag
- DB_PASS=your_password
- DB_NAME=wallabag
- CHECK_INTERVAL=300 # チェック間隔(秒)
- TZ=Asia/Tokyo
restart: unless-stopped
6. 起動
docker-compose up -d --build wallabag-ytthumb
7. 動作確認
docker logs wallabag-ytthumb
以下のようなログが出れば成功です:
YouTube Thumbnail Injector started (interval: 300s)
[UPDATE] id=16 -> https://img.youtube.com/vi/xxxxx/maxresdefault.jpg
対応URL形式
以下のYouTube URL形式に対応しています:
https://www.youtube.com/watch?v=VIDEO_IDhttps://youtu.be/VIDEO_IDhttps://www.youtube.com/shorts/VIDEO_ID- URLエンコードされた形式(
v%3DVIDEO_ID)
サムネイル解像度について
maxresdefault.jpgは最高解像度ですが、一部の動画では存在しない場合があります。その場合は以下に変更できます:
| ファイル名 | 解像度 |
|---|---|
| maxresdefault.jpg | 1280x720 |
| sddefault.jpg | 640x480 |
| hqdefault.jpg | 480x360 |
| mqdefault.jpg | 320x180 |
| default.jpg | 120x90 |
確実性を重視するならhqdefault.jpgがおすすめです。
まとめ
- Wallabag単体ではYouTubeのサムネイル取得が不安定
-
img.youtube.comを使えば確実にサムネイルを取得可能 - 小さなDockerコンテナを追加するだけで自動化できる
これでWallabagのリスト表示がより見やすくなります。
