[webpages] Add images to webpages
All checks were successful
build & deploy / test (push) Successful in 1m41s
build & deploy / deploy (push) Successful in 22s

This commit is contained in:
2026-03-14 13:54:27 -04:00
parent a5ff6abf56
commit 223de52a12
2 changed files with 89 additions and 16 deletions

View File

@ -0,0 +1,20 @@
# Generated by Django 4.2.29 on 2026-03-14 17:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("webpages", "0006_remove_webpage_run_time_seconds_and_more"),
]
operations = [
migrations.AddField(
model_name="webpage",
name="image",
field=models.ImageField(
blank=True, null=True, upload_to="webpage/"
),
),
]

View File

@ -1,19 +1,23 @@
import logging import logging
from typing import Dict from typing import Dict, Optional
from uuid import uuid4 from uuid import uuid4
from django_extensions.db.models import TimeStampedModel
import pendulum import pendulum
import requests import requests
from taggit.managers import TaggableManager
import trafilatura import trafilatura
from bs4 import BeautifulSoup
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django_extensions.db.models import TimeStampedModel
from htmldate import find_date from htmldate import find_date
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit
from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
from taggit.managers import TaggableManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
BNULL = {"blank": True, "null": True} BNULL = {"blank": True, "null": True}
@ -35,9 +39,9 @@ class Domain(TimeStampedModel):
def scrobbles_for_user(self, user_id): def scrobbles_for_user(self, user_id):
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
return Scrobble.objects.filter( return Scrobble.objects.filter(web_page__domain=self, user_id=user_id).order_by(
web_page__domain=self, user_id=user_id "-timestamp"
).order_by("-timestamp") )
class WebPage(ScrobblableMixin): class WebPage(ScrobblableMixin):
@ -49,6 +53,20 @@ class WebPage(ScrobblableMixin):
domain = models.ForeignKey(Domain, on_delete=models.DO_NOTHING, **BNULL) domain = models.ForeignKey(Domain, on_delete=models.DO_NOTHING, **BNULL)
extract = models.TextField(**BNULL) extract = models.TextField(**BNULL)
image = models.ImageField(upload_to="webpage/", **BNULL)
image_small = ImageSpecField(
source="image",
processors=[ResizeToFit(100, 100)],
format="JPEG",
options={"quality": 60},
)
image_medium = ImageSpecField(
source="image",
processors=[ResizeToFit(300, 300)],
format="JPEG",
options={"quality": 75},
)
def __str__(self) -> str: def __str__(self) -> str:
if self.title: if self.title:
return "{} ({})".format(self.title, self.domain) return "{} ({})".format(self.title, self.domain)
@ -95,7 +113,8 @@ class WebPage(ScrobblableMixin):
@property @property
def primary_image_url(self) -> str: def primary_image_url(self) -> str:
# TODO Figure out how to add a preview? if self.image:
return self.image.url
return "" return ""
def scrobble_for_user(self, user_id): def scrobble_for_user(self, user_id):
@ -117,9 +136,7 @@ class WebPage(ScrobblableMixin):
def scrobbles(self, user): def scrobbles(self, user):
Scrobble = apps.get_model("scrobbles", "Scrobble") Scrobble = apps.get_model("scrobbles", "Scrobble")
return Scrobble.objects.filter(user=user, web_page=self).order_by( return Scrobble.objects.filter(user=user, web_page=self).order_by("-timestamp")
"-timestamp"
)
def clean_title(self, title: str, save=True): def clean_title(self, title: str, save=True):
if len(title.split("|")) > 1: if len(title.split("|")) > 1:
@ -144,9 +161,7 @@ class WebPage(ScrobblableMixin):
if not raw_text: if not raw_text:
return return
self.title = raw_text[ self.title = raw_text[raw_text.find("<title>") + 7 : raw_text.find("</title>")]
raw_text.find("<title>") + 7 : raw_text.find("</title>")
]
if not self.title and self.extract: if not self.title and self.extract:
first_line = self.extract.split("\n")[0] first_line = self.extract.split("\n")[0]
@ -194,14 +209,47 @@ class WebPage(ScrobblableMixin):
return return
if response.status_code == 200: if response.status_code == 200:
logger.info( logger.info("Website already exists in archive", extra={"url": self.url})
"Website already exists in archive", extra={"url": self.url}
)
else: else:
raise Exception( raise Exception(
f"Failed to push URL to archivebox (Response {response.status_code})" f"Failed to push URL to archivebox (Response {response.status_code})"
) )
def _extract_image_url_from_html(self, raw_html: str) -> Optional[str]:
"""Extract og:image or twitter:image from HTML."""
if not raw_html:
return None
soup = BeautifulSoup(raw_html, "html.parser")
og_image = soup.find("meta", property="og:image")
if og_image and og_image.get("content"):
return og_image["content"]
twitter_image = soup.find("meta", attrs={"name": "twitter:image"})
if twitter_image and twitter_image.get("content"):
return twitter_image["content"]
return None
def _download_and_save_image(self, image_url: str) -> bool:
"""Download and save the webpage image."""
if not image_url:
return False
try:
response = requests.get(image_url, timeout=10)
response.raise_for_status()
filename = image_url.split("/")[-1].split("?")[0]
if not filename.endswith((".jpg", ".jpeg", ".png", ".webp")):
filename = "webpage_image.jpg"
self.image.save(filename, ContentFile(response.content), save=True)
return True
except Exception:
return False
def fetch_data_from_web(self, save=True, force=True): def fetch_data_from_web(self, save=True, force=True):
raw_text = trafilatura.fetch_url(self.url) raw_text = trafilatura.fetch_url(self.url)
if not self.extract or force: if not self.extract or force:
@ -223,6 +271,11 @@ class WebPage(ScrobblableMixin):
if not self.base_run_time_seconds or force: if not self.base_run_time_seconds or force:
self.base_run_time_seconds = self.estimated_time_to_read_in_seconds self.base_run_time_seconds = self.estimated_time_to_read_in_seconds
if (not self.image or force) and raw_text:
image_url = self._extract_image_url_from_html(raw_text)
if image_url:
self._download_and_save_image(image_url)
if save: if save:
self.save() self.save()