Add YouTube thumbnail support

Fixes #359
This commit is contained in:
rubenwardy 2024-06-07 06:25:32 +01:00
parent ee83a7b5ce
commit 5bd97598a8
6 changed files with 67 additions and 7 deletions

@ -14,7 +14,9 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import abort, send_file, Blueprint, current_app import re
import requests
from flask import abort, send_file, Blueprint, current_app, request
import os import os
from PIL import Image from PIL import Image
@ -105,3 +107,26 @@ def make_thumbnail(img, level):
res = send_file(cache_filepath) res = send_file(cache_filepath)
res.headers["Cache-Control"] = "max-age=604800" # 1 week res.headers["Cache-Control"] = "max-age=604800" # 1 week
return res return res
@bp.route("/thumbnails/youtube/<id_>.jpg")
def youtube(id_: str):
if not re.match(r"^[A-Za-z0-9\-_]+$", id_):
abort(400)
cache_dir = os.path.join(current_app.config["THUMBNAIL_DIR"], "youtube")
os.makedirs(cache_dir, exist_ok=True)
cache_filepath = os.path.join(cache_dir, id_ + ".jpg")
url = f"https://img.youtube.com/vi/{id_}/default.jpg"
response = requests.get(url, stream=True)
if response.status_code != 200:
abort(response.status_code)
with open(cache_filepath, "wb") as file:
file.write(response.content)
res = send_file(cache_filepath)
res.headers["Cache-Control"] = "max-age=604800" # 1 week
return res

@ -392,6 +392,19 @@ class Package(db.Model):
def donate_url_actual(self): def donate_url_actual(self):
return self.donate_url or self.author.donate_url return self.donate_url or self.author.donate_url
@property
def video_thumbnail_url(self):
from app.utils.url import get_youtube_id
if self.video_url is None:
return None
id_ = get_youtube_id(self.video_url)
if id_:
return url_for("thumbnails.youtube", id_=id_)
return None
enable_game_support_detection = db.Column(db.Boolean, nullable=False, default=True) enable_game_support_detection = db.Column(db.Boolean, nullable=False, default=True)
translations = db.relationship("PackageTranslation", back_populates="package", translations = db.relationship("PackageTranslation", back_populates="package",

@ -52,10 +52,21 @@
justify-content: center !important; justify-content: center !important;
cursor: pointer; cursor: pointer;
img {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
object-fit: cover;
z-index: 10;
}
.fa-play { .fa-play {
display: block; display: block;
font-size: 200%; font-size: 200%;
color: #f44; color: #f44;
z-index: 20;
} }
&:hover { &:hover {
@ -72,6 +83,7 @@
right: 0.5rem; right: 0.5rem;
color: #555; color: #555;
font-size: 80%; font-size: 80%;
z-index: 30;
} }
} }

@ -16,7 +16,7 @@
{%- endif %} {%- endif %}
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css?v=4"> <link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css?v=4">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=51"> <link rel="stylesheet" type="text/css" href="/static/custom.css?v=52">
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" /> <link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
{% if noindex -%} {% if noindex -%}

@ -292,6 +292,10 @@
{% if package.video_url %} {% if package.video_url %}
<li> <li>
<a href="{{ package.video_url }}" class="video-embed"> <a href="{{ package.video_url }}" class="video-embed">
{% set thumbnail_url = package.video_thumbnail_url %}
{% if thumbnail_url %}
<img src="{{ thumbnail_url }}" alt="{{ _('Thumbnail for video') }}" />
{% endif %}
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
<div class="label"> <div class="label">
<i class="fas fa-external-link-square-alt"></i> <i class="fas fa-external-link-square-alt"></i>

@ -31,16 +31,22 @@ def url_get_query(parsed_url: urlparse.ParseResult) -> Dict[str, List[str]]:
return urlparse.parse_qs(parsed_url.query) return urlparse.parse_qs(parsed_url.query)
def clean_youtube_url(url: str) -> Optional[str]: def get_youtube_id(url: str) -> Optional[str]:
parsed = urlparse.urlparse(url) parsed = urlparse.urlparse(url)
print(parsed)
if (parsed.netloc == "www.youtube.com" or parsed.netloc == "youtube.com") and parsed.path == "/watch": if (parsed.netloc == "www.youtube.com" or parsed.netloc == "youtube.com") and parsed.path == "/watch":
print(url_get_query(parsed))
video_id = url_get_query(parsed).get("v", [None])[0] video_id = url_get_query(parsed).get("v", [None])[0]
if video_id: if video_id:
return url_set_query("https://www.youtube.com/watch", {"v": video_id}) return video_id
elif parsed.netloc == "youtu.be": elif parsed.netloc == "youtu.be":
return url_set_query("https://www.youtube.com/watch", {"v": parsed.path[1:]}) return parsed.path[1:]
return None
def clean_youtube_url(url: str) -> Optional[str]:
id_ = get_youtube_id(url)
if id_:
return url_set_query("https://www.youtube.com/watch", {"v": id_})
return None return None