Add Atom and JSON feeds for releases and new packages

Fixes #224
This commit is contained in:
rubenwardy 2024-07-02 20:41:39 +01:00
parent e43a7827c2
commit f8abcaa7c6
7 changed files with 273 additions and 3 deletions

@ -0,0 +1,146 @@
# ContentDB
# Copyright (C) 2024 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from flask import Blueprint, jsonify, render_template, make_response
from flask_babel import gettext
from app.markdown import render_markdown
from app.models import Package, PackageState, db, PackageRelease
from app.utils import is_package_page, abs_url_for
bp = Blueprint("feeds", __name__)
def _make_feed(title: str, feed_url: str, items: list):
return {
"version": "https://jsonfeeds.org/version/1",
"title": title,
"description": gettext("Welcome to the best place to find Minetest mods, games, and texture packs"),
"home_page_url": "https://content.minetest.net/",
"feed_url": feed_url,
"icon": "https://content.minetest.net/favicon-128.png",
"expired": False,
"items": items,
}
def _get_new_packages_feed(feed_url: str) -> dict:
packages = (Package.query
.filter(Package.state == PackageState.APPROVED)
.order_by(db.desc(Package.approved_at))
.limit(100)
.all())
items = [{
"id": package.get_url("packages.view", absolute=True),
"language": "en",
"title": f"New: {package.title}",
"content_html": render_markdown(package.desc) if package.desc else None,
"author": package.author.display_name,
"url": package.get_url("packages.view", absolute=True),
"summary": package.short_desc,
"date_published": package.approved_at.isoformat(timespec="seconds") + "Z",
"tags": ["new_package"],
} for package in packages]
return _make_feed(gettext("ContentDB new packages"), feed_url, items)
def _get_releases_feed(query, feed_url: str):
releases = (query
.filter(PackageRelease.package.has(state=PackageState.APPROVED), PackageRelease.approved==True)
.order_by(db.desc(PackageRelease.created_at))
.limit(250)
.all())
items = [{
"id": release.package.get_url("packages.view_release", id=release.id, absolute=True),
"language": "en",
"title": f"{release.title} - {release.package.title}",
"content_html": render_markdown(release.release_notes) if release.release_notes else None,
"author": release.package.author.display_name,
"url": release.package.get_url("packages.view_release", id=release.id, absolute=True),
"summary": release.summary,
"date_published": release.created_at.isoformat(timespec="seconds") + "Z",
"tags": ["release"],
} for release in releases]
return _make_feed(gettext("ContentDB package updates"), feed_url, items)
def _get_all_feed(feed_url: str):
releases = _get_releases_feed(PackageRelease.query, "")["items"]
packages = _get_new_packages_feed("")["items"]
items = releases + packages
return _make_feed(gettext("ContentDB all"), feed_url, items)
def _atomify(feed):
resp = make_response(render_template("feeds/json_to_atom.xml", feed=feed))
resp.headers["Content-type"] = "application/atom+xml; charset=utf-8"
return resp
@bp.route("/feeds/all.json")
def all_json():
feed = _get_all_feed(abs_url_for("feeds.all_json"))
return jsonify(feed)
@bp.route("/feeds/all.atom")
def all_atom():
feed = _get_all_feed(abs_url_for("feeds.all_atom"))
return _atomify(feed)
@bp.route("/feeds/packages.json")
def packages_all_json():
feed = _get_new_packages_feed(abs_url_for("feeds.packages_all_json"))
return jsonify(feed)
@bp.route("/feeds/packages.atom")
def packages_all_atom():
feed = _get_new_packages_feed(abs_url_for("feeds.packages_all_atom"))
return _atomify(feed)
@bp.route("/feeds/releases.json")
def releases_all_json():
feed = _get_releases_feed(PackageRelease.query, abs_url_for("feeds.releases_all_json"))
return jsonify(feed)
@bp.route("/feeds/releases.atom")
def releases_all_atom():
feed = _get_releases_feed(PackageRelease.query, abs_url_for("feeds.releases_all_atom"))
return _atomify(feed)
@bp.route("/packages/<author>/<name>/releases_feed.json")
@is_package_page
def releases_package_json(package: Package):
feed = _get_releases_feed(package.releases, package.get_url("feeds.releases_package_json", absolute=True))
return jsonify(feed)
@bp.route("/packages/<author>/<name>/releases_feed.atom")
@is_package_page
def releases_package_atom(package: Package):
feed = _get_releases_feed(package.releases, package.get_url("feeds.releases_package_atom", absolute=True))
return _atomify(feed)

@ -156,11 +156,21 @@ def download_release(package, id):
return redirect(release.url) return redirect(release.url)
@bp.route("/packages/<author>/<name>/releases/<int:id>/", methods=["GET", "POST"]) @bp.route("/packages/<author>/<name>/releases/<int:id>/")
@is_package_page
def view_release(package, id):
release: PackageRelease = PackageRelease.query.get(id)
if release is None or release.package != package:
abort(404)
return render_template("packages/release_view.html", package=package, release=release)
@bp.route("/packages/<author>/<name>/releases/<int:id>/edit/", methods=["GET", "POST"])
@login_required @login_required
@is_package_page @is_package_page
def edit_release(package, id): def edit_release(package, id):
release : PackageRelease = PackageRelease.query.get(id) release: PackageRelease = PackageRelease.query.get(id)
if release is None or release.package != package: if release is None or release.package != package:
abort(404) abort(404)

@ -1098,6 +1098,15 @@ class PackageRelease(db.Model):
downloads = db.Column(db.Integer, nullable=False, default=0) downloads = db.Column(db.Integer, nullable=False, default=0)
release_notes = db.Column(db.UnicodeText, nullable=True, default=None) release_notes = db.Column(db.UnicodeText, nullable=True, default=None)
@property
def summary(self) -> typing.Optional[str]:
if self.release_notes is None:
return None
if self.release_notes.startswith("-") or self.release_notes.startswith("*"):
return None
return self.release_notes.split("\n")[0]
min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None) min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None)
min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id]) min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id])

@ -20,9 +20,23 @@
<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 -%}
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
{%- endif %} {%- endif %}
<link rel="alternate" type="application/json"
href="{{ abs_url_for('feeds.all_json') }}" title="{{ _('ContentDB all') }}">
<link rel="alternate" type="application/json"
href="{{ abs_url_for('feeds.packages_all_json') }}" title="{{ _('ContentDB new packages') }}">
<link rel="alternate" type="application/json"
href="{{ abs_url_for('feeds.releases_all_json') }}" title="{{ _('ContentDB package updates') }}">
<link rel="alternate" type="application/atom+xml"
href="{{ abs_url_for('feeds.all_atom') }}" title="{{ _('ContentDB all') }}">
<link rel="alternate" type="application/atom+xml"
href="{{ abs_url_for('feeds.packages_all_atom') }}" title="{{ _('ContentDB new packages') }}">
<link rel="alternate" type="application/atom+xml"
href="{{ abs_url_for('feeds.releases_all_atom') }}" title="{{ _('ContentDB package updates') }}">
{% block headextra %}{% endblock %} {% block headextra %}{% endblock %}
</head> </head>
<body> <body>

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
<title>{{ feed["title"] }}</title>
<subtitle>{{ feed["description"] }}</subtitle>
<link href="{{ feed["feed_url"] }}" rel="self" />
<link href="{{ feed["home_page_url"] }}" />
<updated>{{ feed["items"][0]["date_published"] }}</updated>
<id>{{ feed["feed_url"] }}</id>
{% if feed["authors"] %}
<author>
<name>{{ feed["authors"][0]["name"] }}</name>
</author>
{% endif %}
{%- for post in feed["items"] %}
<entry>
<title>{{ post["title"] | escape }}</title>
<link href="{{ post["url"] }}" rel="alternate" type="text/html" title="{{ post["title"] | escape }}" />
<published>{{ post["date_published"] }}</published>
<updated>{{ post["date_published"] }}</updated>
<id>{{ post["url"] }}</id>
<summary type="text">
{{ post["summary"] | escape }}
</summary>
<content xml:lang="{{ post["language"] }}" type="html" xml:base="{{ post["url"] }}">
{{ post["content_html"] | escape }}
</content>
<author>
<name>{{ post["author"] }}</name>
</author>
{% for tag in post["tags"] %}
<category term="{{ tag | escape }}" />
{% endfor %}
{% if post["image"] %}
<media:thumbnail url="{{ post["image"] }}" />
<media:content medium="image" url="{{ post["image"] }}" />
{% endif %}
</entry>
{%- endfor %}
</feed>

@ -0,0 +1,45 @@
{% extends "packages/package_base.html" %}
{% block title %}
{{ package.title }}
{% endblock %}
{% block content %}
{% if package.check_perm(current_user, "MAKE_RELEASE") %}
<a href="{{ package.get_url('packages.edit_release', id=release.id) }}" class="btn btn-primary float-end">
{{ _("Edit") }}
</a>
{% endif %}
<h1>{{ self.title() }}</h1>
<p>
<a href="{{ package.get_url('packages.view') }}">
{{ _("%(title)s by %(author)s", title=package.title, author=package.author.display_name) }}
</a>
</p>
<p>
{{ _("Name") }}: {{ release.name }}<br>
{{ _("Title") }}: {{ release.title }}
</p>
{% if release.release_notes %}
<div class="markdown panel my-5">
{{ release.release_notes | markdown }}
</div>
{% endif %}
<p>
{{ _("URL") }}: <a href="{{ release.url }}">{{ release.url }}</a><br />
</p>
{% if release.commit_hash %}
<p>
{{ _("Commit Hash") }}: {{ release.commit_hash }}<br />
</p>
{% endif %}
{% if release.task_id %}
<p>
{{ _("Importing...") }}
<a href="{{ url_for('tasks.check', id=release.task_id, r=release.get_edit_url()) }}">{{ _("view task") }}</a><br />
</p>
{% endif %}
{% endblock %}

@ -16,6 +16,13 @@
{% if package.get_thumb_url(3, True, "png") -%} {% if package.get_thumb_url(3, True, "png") -%}
<meta name="og:image" content="{{ package.get_thumb_url(3, True, "png") }}"/> <meta name="og:image" content="{{ package.get_thumb_url(3, True, "png") }}"/>
{%- endif %} {%- endif %}
<link rel="alternate" type="application/json"
href="{{ package.get_url('feeds.releases_package_json', absolute=True) }}"
title="{{ _('%(title)s releases', title=package.title) }}">
<link rel="alternate" type="application/atom+xml"
href="{{ package.get_url('feeds.releases_package_atom', absolute=True) }}"
title="{{ _('%(title)s releases', title=package.title) }}">
{% endblock %} {% endblock %}
{% block scriptextra %} {% block scriptextra %}