implemented BEP 36 (Torrent RSS feeds) in magneticow

http://www.bittorrent.org/beps/bep_0036.html
This commit is contained in:
Bora M. Alper 2017-06-16 10:54:28 +03:00
parent d877ba2475
commit 65dc6737e1
4 changed files with 88 additions and 2 deletions

View File

@ -16,6 +16,7 @@ import collections
import functools import functools
import datetime as dt import datetime as dt
from datetime import datetime from datetime import datetime
import hashlib
import logging import logging
import sqlite3 import sqlite3
import os import os
@ -41,7 +42,7 @@ magneticod_db = None
# Adapted from: http://flask.pocoo.org/snippets/8/ # Adapted from: http://flask.pocoo.org/snippets/8/
# (c) Copyright 2010 - 2017 by Armin Ronacher # (c) Copyright 2010 - 2017 by Armin Ronacher
# BEGINNING OF THE COPYRIGHTED CONTENT # BEGINNING OF THE COPYRIGHTED CONTENT
def check_auth(supplied_username, supplied_password): def is_authorized(supplied_username, supplied_password):
""" This function is called to check if a username / password combination is valid. """ """ This function is called to check if a username / password combination is valid. """
# Because we do monkey-patch! [in magneticow.__main__.py:main()] # Because we do monkey-patch! [in magneticow.__main__.py:main()]
for username, password in app.arguments.user: # pylint: disable=maybe-no-member for username, password in app.arguments.user: # pylint: disable=maybe-no-member
@ -64,13 +65,24 @@ def requires_auth(f):
@functools.wraps(f) @functools.wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
auth = flask.request.authorization auth = flask.request.authorization
if not auth or not check_auth(auth.username, auth.password): if not auth or not is_authorized(auth.username, auth.password):
return authenticate() return authenticate()
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated return decorated
# END OF THE COPYRIGHTED CONTENT # END OF THE COPYRIGHTED CONTENT
def generate_feed_hash(username: str, password: str, filter_: str) -> str:
"""
Deterministically generates the feed hash from given username, password, and filter.
Hash is the hex encoding of the SHA256 sum.
:param username:
:param password:
:param filter_:
:return:
"""
return hashlib.sha256((username + "\0" + password + "\0" + filter_).encode()).digest().hex()
@app.route("/") @app.route("/")
@requires_auth @requires_auth
def home_page(): def home_page():
@ -128,6 +140,9 @@ def search_torrents():
else: else:
context["next_page_exists"] = True context["next_page_exists"] = True
username, password = flask.request.authorization.username, flask.request.authorization.password
context["subscription_url"] = "/feed?filter=%s&hash=%s" % (search, generate_feed_hash(username, password, search))
return flask.render_template("torrents.html", **context) return flask.render_template("torrents.html", **context)
@ -158,6 +173,9 @@ def newest_torrents():
else: else:
context["next_page_exists"] = True context["next_page_exists"] = True
username, password = flask.request.authorization.username, flask.request.authorization.password
context["subscription_url"] = "/feed?filter=&hash=%s" % (generate_feed_hash(username, password, ""),)
return flask.render_template("torrents.html", **context) return flask.render_template("torrents.html", **context)
@ -241,6 +259,56 @@ def statistics():
}) })
@app.route("/feed")
def feed():
filter_ = flask.request.args["filter"]
hash_ = flask.request.args["hash"]
# Check for all possible users who might be requesting.
# pylint disabled: because we do monkey-patch! [in magneticow.__main__.py:main()]
for username, password in app.arguments.user: # pylint: disable=maybe-no-member
if generate_feed_hash(username, password, filter_) == hash_:
break
else:
return flask.Response(
"Could not verify your access level for that URL (wrong hash).\n",
401
)
context = {}
if filter_:
context["title"] = "`%s` - magneticow" % (filter_,)
with magneticod_db:
cur = magneticod_db.execute(
"SELECT "
" name, "
" info_hash "
"FROM torrents "
"INNER JOIN ("
" SELECT docid AS id, rank(matchinfo(fts_torrents, 'pcnxal')) AS rank "
" FROM fts_torrents "
" WHERE name MATCH ? "
" ORDER BY rank ASC"
" LIMIT 50"
") AS ranktable USING(id);",
(filter_, )
)
context["items"] = [{"title": r[0], "info_hash": r[1].hex()} for r in cur]
else:
context["title"] = "The Newest Torrents - magneticow"
with magneticod_db:
cur = magneticod_db.execute(
"SELECT "
" name, "
" info_hash "
"FROM torrents "
"ORDER BY id DESC LIMIT 50"
)
context["items"] = [{"title": r[0], "info_hash": r[1].hex()} for r in cur]
return flask.render_template("feed.xml", **context), 200, {"Content-Type": "application/rss+xml; charset=utf-8"}
def initialize_magneticod_db() -> None: def initialize_magneticod_db() -> None:
global magneticod_db global magneticod_db

View File

@ -22,6 +22,7 @@ header form {
width: 100%; width: 100%;
margin-left: 0.5em; margin-left: 0.5em;
margin-right: 0.5em;
} }

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0">
<channel>
<title>{{ title }}</title>
{% for item in items %}
<item>
<title>{{ item.title }}</title>
<guid>{{ item.info_hash }}</guid>
<enclosure url="magnet:?xt=urn:btih:{{ item.info_hash }}&amp;dn={{ item.title }}" type="application/x-bittorrent" />
</item>
{% endfor %}
</channel>
</rss>

View File

@ -14,6 +14,10 @@
<form action="/torrents" method="get" autocomplete="off" role="search"> <form action="/torrents" method="get" autocomplete="off" role="search">
<input type="search" name="search" placeholder="Search the BitTorrent DHT" value="{{ search }}"> <input type="search" name="search" placeholder="Search the BitTorrent DHT" value="{{ search }}">
</form> </form>
<div>
<a href="{{ subscription_url }}"><img src="{{ url_for('static', filename='assets/feed.png') }}"
alt="feed icon" title="subscribe" /> subscribe</a>
</div>
</header> </header>
<main> <main>
<table> <table>