fix for the feed, and added sorting support!

This commit is contained in:
Bora M. Alper 2017-07-04 18:51:46 +03:00
parent 07fe0d3eb4
commit 2b99fb3675
3 changed files with 146 additions and 108 deletions

View File

@ -0,0 +1,60 @@
# magneticow - Lightweight web interface for magnetico.
# Copyright (C) 2017 Mert Bora ALPER <bora@boramalper.org>
# Dedicated to Cemile Binay, in whose hands I thrived.
#
# 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
# <http://www.gnu.org/licenses/>.
import functools
import hashlib
import flask
# Adapted from: http://flask.pocoo.org/snippets/8/
# (c) Copyright 2010 - 2017 by Armin Ronacher
# BEGINNING OF THE 3RD PARTY COPYRIGHTED CONTENT
def is_authorized(supplied_username, supplied_password):
""" This function is called to check if a username / password combination is valid. """
# Because we do monkey-patch! [in magneticow.__main__.py:main()]
app = flask.current_app
for username, password in app.arguments.user: # pylint: disable=maybe-no-member
if supplied_username == username and supplied_password == password:
return True
return False
def authenticate():
""" Sends a 401 response that enables basic auth. """
return flask.Response(
"Could not verify your access level for that URL.\n"
"You have to login with proper credentials",
401,
{"WWW-Authenticate": 'Basic realm="Login Required"'}
)
def requires_auth(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
auth = flask.request.authorization
if not auth or not is_authorized(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
# END OF THE 3RD PARTY 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.
"""
return hashlib.sha256((username + "\0" + password + "\0" + filter_).encode()).digest().hex()

View File

@ -13,10 +13,8 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <http://www.gnu.org/licenses/>. # <http://www.gnu.org/licenses/>.
import collections import collections
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
@ -25,6 +23,7 @@ import appdirs
import flask import flask
from magneticow import utils from magneticow import utils
from magneticow.authorization import requires_auth, generate_feed_hash
File = collections.namedtuple("file", ["path", "size"]) File = collections.namedtuple("file", ["path", "size"])
@ -38,51 +37,6 @@ app.config.from_object(__name__)
# this. Investigate the cause and fix it (I suspect of Gevent). # this. Investigate the cause and fix it (I suspect of Gevent).
magneticod_db = None magneticod_db = None
# Adapted from: http://flask.pocoo.org/snippets/8/
# (c) Copyright 2010 - 2017 by Armin Ronacher
# BEGINNING OF THE COPYRIGHTED CONTENT
def is_authorized(supplied_username, supplied_password):
""" This function is called to check if a username / password combination is valid. """
# Because we do monkey-patch! [in magneticow.__main__.py:main()]
for username, password in app.arguments.user: # pylint: disable=maybe-no-member
if supplied_username == username and supplied_password == password:
return True
return False
def authenticate():
""" Sends a 401 response that enables basic auth. """
return flask.Response(
"Could not verify your access level for that URL.\n"
"You have to login with proper credentials",
401,
{"WWW-Authenticate": 'Basic realm="Login Required"'}
)
def requires_auth(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
auth = flask.request.authorization
if not auth or not is_authorized(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
# 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():
@ -97,16 +51,7 @@ def home_page():
@app.route("/torrents/") @app.route("/torrents/")
@requires_auth @requires_auth
def torrents(): def torrents():
if flask.request.args: search = flask.request.args.get("search")
if flask.request.args["search"] == "":
return newest_torrents()
return search_torrents()
else:
return newest_torrents()
def search_torrents():
search = flask.request.args["search"]
page = int(flask.request.args.get("page", 0)) page = int(flask.request.args.get("page", 0))
context = { context = {
@ -114,23 +59,56 @@ def search_torrents():
"page": page "page": page
} }
SQL_query = """
SELECT
info_hash,
name,
total_size,
discovered_on
FROM torrents
"""
if search:
SQL_query += """
INNER JOIN (
SELECT docid AS id, rank(matchinfo(fts_torrents, 'pcnxal')) AS rank
FROM fts_torrents
WHERE name MATCH ?
) AS ranktable USING(id)
"""
SQL_query += """
ORDER BY {}
LIMIT 20 OFFSET ?
"""
sort_by = flask.request.args.get("sort_by")
allowed_sorts = [
None,
"name ASC",
"name DESC",
"total_size ASC",
"total_size DESC",
"discovered_on ASC",
"discovered_on DESC"
]
if sort_by not in allowed_sorts:
return flask.Response("Invalid value for `sort_by! (Allowed values are %s)" % (allowed_sorts, ), 400)
if search:
if sort_by:
SQL_query = SQL_query.format(sort_by + ", " + "rank ASC")
else:
SQL_query = SQL_query.format("rank ASC")
else:
if sort_by:
SQL_query = SQL_query.format(sort_by + ", " + "id DESC")
else:
SQL_query = SQL_query.format("id DESC")
with magneticod_db: with magneticod_db:
cur = magneticod_db.execute( if search:
"SELECT" cur = magneticod_db.execute(SQL_query, (search, 20 * page))
" info_hash, " else:
" name, " cur = magneticod_db.execute(SQL_query, (20 * page, ))
" total_size, "
" discovered_on "
"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 20 OFFSET ?"
") AS ranktable USING(id);",
(search, 20 * page)
)
context["torrents"] = [Torrent(t[0].hex(), t[1], utils.to_human_size(t[2]), context["torrents"] = [Torrent(t[0].hex(), t[1], utils.to_human_size(t[2]),
datetime.fromtimestamp(t[3]).strftime("%d/%m/%Y"), []) datetime.fromtimestamp(t[3]).strftime("%d/%m/%Y"), [])
for t in cur.fetchall()] for t in cur.fetchall()]
@ -143,38 +121,8 @@ def search_torrents():
username, password = flask.request.authorization.username, flask.request.authorization.password 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)) context["subscription_url"] = "/feed?filter=%s&hash=%s" % (search, generate_feed_hash(username, password, search))
return flask.render_template("torrents.html", **context) if sort_by:
context["sorted_by"] = sort_by
def newest_torrents():
page = int(flask.request.args.get("page", 0))
context = {
"page": page
}
with magneticod_db:
cur = magneticod_db.execute(
"SELECT "
" info_hash, "
" name, "
" total_size, "
" discovered_on "
"FROM torrents "
"ORDER BY id DESC LIMIT 20 OFFSET ?",
(20 * page,)
)
context["torrents"] = [Torrent(t[0].hex(), t[1], utils.to_human_size(t[2]), datetime.fromtimestamp(t[3]).strftime("%d/%m/%Y"), [])
for t in cur.fetchall()]
# noinspection PyTypeChecker
if len(context["torrents"]) < 20:
context["next_page_exists"] = False
else:
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)

View File

@ -24,9 +24,33 @@
<thead> <thead>
<tr> <tr>
<th><!-- Magnet link --></th> <th><!-- Magnet link --></th>
<th>Name</th> <th>
<th>Size</th> {% if sorted_by == "name ASC" %}
<th>Discovered on</th> <a href="/torrents/?search={{ search }}&sort_by=name+DESC">Name ▲</a>
{% elif sorted_by == "name DESC" %}
<a href="/torrents/?search={{ search }}&sort_by=name+ASC">Name ▼</a>
{% else %}
<a href="/torrents/?search={{ search }}&sort_by=name+ASC">Name</a>
{% endif %}
</th>
<th>
{% if sorted_by == "total_size ASC" %}
<a href="/torrents/?search={{ search }}&sort_by=total_size+DESC">Size ▲</a>
{% elif sorted_by == "total_size DESC" %}
<a href="/torrents/?search={{ search }}&sort_by=total_size+ASC">Size ▼</a>
{% else %}
<a href="/torrents/?search={{ search }}&sort_by=total_size+ASC">Size</a>
{% endif %}
</th>
<th>
{% if sorted_by == "discovered_on ASC" %}
<a href="/torrents/?search={{ search }}&sort_by=discovered_on+DESC">Discovered on ▲</a>
{% elif sorted_by == "discovered_on DESC" %}
<a href="/torrents/?search={{ search }}&sort_by=discovered_on+ASC">Discovered on ▼</a>
{% else %}
<a href="/torrents/?search={{ search }}&sort_by=discovered_on+DESC">Discovered on</a>
{% endif %}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -48,11 +72,17 @@
<form action="/torrents" method="get"> <form action="/torrents" method="get">
<button {% if page == 0 %}disabled{% endif %}>Previous</button> <button {% if page == 0 %}disabled{% endif %}>Previous</button>
<input type="text" name="search" value="{{ search }}" hidden> <input type="text" name="search" value="{{ search }}" hidden>
{% if sorted_by %}
<input type="text" name="sort_by" value="{{ sorted_by }}" hidden>
{% endif %}
<input type="number" name="page" value="{{ page - 1 }}" hidden> <input type="number" name="page" value="{{ page - 1 }}" hidden>
</form> </form>
<form action="/torrents" method="get"> <form action="/torrents" method="get">
<button {% if not next_page_exists %}disabled{% endif %}>Next</button> <button {% if not next_page_exists %}disabled{% endif %}>Next</button>
<input type="text" name="search" value="{{ search }}" hidden> <input type="text" name="search" value="{{ search }}" hidden>
{% if sorted_by %}
<input type="text" name="sort_by" value="{{ sorted_by }}" hidden>
{% endif %}
<input type="number" name="page" value="{{ page + 1 }}" hidden> <input type="number" name="page" value="{{ page + 1 }}" hidden>
</form> </form>
</footer> </footer>