Source code for nefelibata.assistants.twitter_card
import hashlib
import mimetypes
import re
import urllib.parse
from pathlib import Path
from typing import Tuple
import requests
from bs4 import BeautifulSoup
from mutagen.mp3 import MP3
from nefelibata.assistants import Assistant
from nefelibata.assistants import Scope
from nefelibata.post import Post
from nefelibata.utils import modify_html
from PIL import Image
[docs]def has_valid_dimensions(path: Path) -> bool:
im = Image.open(path)
width, height = im.size # type: Tuple[int, int]
return 144 <= width <= 4096 and 144 <= height <= 4096
[docs]class TwitterCardAssistant(Assistant):
scopes = [Scope.POST]
container_filename = "twitter_card.html"
[docs] def process_post(self, post: Post, force: bool = False) -> None:
file_path = post.file_path.with_suffix(".html")
post_directory = post.file_path.parent
card_metadata = {
"twitter:card": "summary",
"twitter:site": f'@{self.config["twitter"]["handle"]}',
"twitter:title": "No title",
"twitter:description": "No description",
# "twitter:image": "",
# "twitter:image:alt": "",
}
# find media in post
mp3_paths = list(post_directory.glob("**/*.mp3"))
jpg_paths = list(post_directory.glob("**/*.jpg"))
# if the post has a single mp3 use a player card instead of summary
if len(mp3_paths) == 1:
container_path = post_directory / self.container_filename
container_url = urllib.parse.urljoin(
self.config["url"],
str(container_path.relative_to(self.root / "posts")),
)
self._create_container(container_path, mp3_paths[0])
card_metadata["twitter:card"] = "player"
card_metadata["twitter:image"] = urllib.parse.urljoin(
self.config["url"], "img/cassette.png",
)
card_metadata["twitter:player"] = container_url
card_metadata["twitter:player:width"] = "800"
card_metadata["twitter:player:height"] = "464" # 463.15
# otherwise try to use an image
elif jpg_paths:
for jpg_path in jpg_paths:
if has_valid_dimensions(jpg_path):
card_metadata["twitter:image"] = urllib.parse.urljoin(
self.config["url"],
str(jpg_path.relative_to(self.root / "posts")),
)
break
# add meta tags
soup: BeautifulSoup
with modify_html(file_path) as soup:
# collect metadata from the existing page
for attribute in ["title", "description"]:
meta = soup.head.find("meta", {"property": f"og:{attribute}"})
if meta:
card_metadata[f"twitter:{attribute}"] = meta.attrs["content"]
# add new meta tags with the metadata
for name, content in card_metadata.items():
existing = soup.head.find("meta", {"name": name})
if existing:
if existing.attrs["content"] == content:
continue
else:
existing.decompose()
meta = soup.new_tag("meta", attrs={"name": name, "content": content})
soup.head.append(meta)
def _create_container(self, container_path: Path, mp3_path: Path) -> None:
mp3 = MP3(mp3_path)
# create container.html
if not container_path.exists():
with open(container_path, "w") as fp:
fp.write("<!DOCTYPE html><html><head></head><body></body></html>")
with modify_html(container_path) as soup:
css = soup.head.find("link")
if not css:
css = soup.new_tag(
"link",
href=urllib.parse.urljoin(self.config["url"], "css/cassette.css"),
rel="stylesheet",
)
soup.head.append(css)
audio = soup.find("audio")
if audio:
audio.decompose()
audio = soup.new_tag(
"audio",
attrs={
"class": "cassette",
"data-album": mp3.get("TALB", "Unknown album"),
"data-artist": mp3.get("TPE1", "Unknown artist"),
"data-title": mp3.get("TIT2", mp3_path.stem),
"src": mp3_path.relative_to(container_path.parent),
},
)
# do not append, since <script> tag should come last
soup.body.insert(0, audio)
js = soup.body.find("script")
if not js:
js = soup.new_tag(
"script",
attrs={
"src": urllib.parse.urljoin(
self.config["url"], "js/cassette.js",
),
"async": "",
},
)
soup.body.append(js)