fix for the feed, and added sorting support!
This commit is contained in:
parent
07fe0d3eb4
commit
2b99fb3675
60
magneticow/magneticow/authorization.py
Normal file
60
magneticow/magneticow/authorization.py
Normal 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()
|
@ -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)
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user