aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan <me@jordan.im>2021-06-16 03:04:08 -0700
committerGitHub <noreply@github.com>2021-06-16 03:04:08 -0700
commit90e0edf9865fab9822864d1f00d36c4e28a91847 (patch)
tree2cad90e05483e1b9eeeff827739b41244266938b
parent4cd0884b307e28bd9f2e531c41fb7b41600b02a4 (diff)
parente7f4b527e2ee2cbae9f7a53f2a8100fa40bc9138 (diff)
downloadroka-90e0edf9865fab9822864d1f00d36c4e28a91847.tar.gz
roka-90e0edf9865fab9822864d1f00d36c4e28a91847.zip
Merge pull request #3 from dgarrett/static-generation
Static generation
-rw-r--r--.gitignore1
-rw-r--r--README.md25
-rw-r--r--app.cfg.example2
-rw-r--r--lib/books.py10
-rw-r--r--lib/util.py7
-rwxr-xr-xroka.py69
-rw-r--r--templates/index.html2
7 files changed, 100 insertions, 16 deletions
diff --git a/.gitignore b/.gitignore
index f6e84b2..46b2adb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,6 @@
__pycache__
cache
sandbox
+static
app.cfg
uwsgi.ini
diff --git a/README.md b/README.md
index 75572fe..93455fe 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,26 @@ A screenshot of the web interface is [available here](screenshots/web.png).
./uwsgi.sh
```
+## Static generation
+
+In addition to running as a server, Roka can also generate a static index and
+set of RSS feeds that can be deployed to static hosting. This mode does not
+support a username and password.
+
+1. Set `BASE_URL` in app.cfg to the base url where the static site will be
+ uploaded.
+
+2. Run roka.py with the `--generate <output_directory>` parameter, where
+ `<output_directory>` is an output directory to place the generated site. All
+ audiobook files will be copied to this location.
+
+ ```bash
+ ./roka.py --generate ./static
+ ```
+
+3. Upload the static site to any static web hosting. Make sure it is accessible
+ at the URL set as `BASE_URL`
+
## Design decisions
1. Directories contained within config:ROOT_PATH are marked as audiobooks if and
@@ -51,3 +71,8 @@ A screenshot of the web interface is [available here](screenshots/web.png).
4. No rebuild endpoint exists; cache-affecting routines are run externally by
calling roka.py directly
+
+5. Configuration can either be placed in a file named `app.cfg`, or it can be
+ overridden on the terminal by passing a JSON string as the `--config`
+ parameter. I.E. `./roka.py --generate ./static --config '{"ROOT_PATH":
+ "/path/to/audiobooks", "BASE_URL": "https://example.com/"}'`
diff --git a/app.cfg.example b/app.cfg.example
index 114de0b..05a9144 100644
--- a/app.cfg.example
+++ b/app.cfg.example
@@ -1,4 +1,6 @@
ROOT_PATH = '/path/to/audiobooks'
+# BASE_URL is only used for static generation
+BASE_URL = 'https://example.com/'
USERNAME = 'username'
PASSWORD = 'password'
SHOW_PATH = True
diff --git a/lib/books.py b/lib/books.py
index 1c4b4a5..95df374 100644
--- a/lib/books.py
+++ b/lib/books.py
@@ -13,10 +13,6 @@ ABS_PATH = os.path.dirname(os.path.abspath(__file__))
CACHE_PATH = os.path.join(ABS_PATH, '../', 'cache')
JSON_PATH = os.path.join(CACHE_PATH, 'audiobooks.json')
-# use Flask's config parser, configparser would be hacky
-APP = Flask(__name__)
-APP.config.from_pyfile(os.path.join(ABS_PATH, '../', 'app.cfg'))
-
class Books:
def __init__(self):
'''
@@ -86,7 +82,7 @@ class Books:
now = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
print('%s %s' % (now, msg))
- def scan_books(self):
+ def scan_books(self, audiobook_path=None):
'''
Discover audiobooks under :root_path: and populate books object
@@ -94,7 +90,7 @@ class Books:
(existing content is not re-hashed)
'''
ex = self._get_path_hash_dict()
- dirs = self._get_dirs(APP.config['ROOT_PATH'])
+ dirs = self._get_dirs(audiobook_path)
books = dict()
for path in dirs:
@@ -145,7 +141,7 @@ class Books:
# previous conditions met, we've found at least one track
is_book = True
- self._log(f)
+ self._log(os.path.join(path, f))
# hash track (used as a key) and update folder hash
file_hash = hashlib.md5()
diff --git a/lib/util.py b/lib/util.py
index 1eb6af6..4693ef1 100644
--- a/lib/util.py
+++ b/lib/util.py
@@ -78,9 +78,7 @@ def escape(s):
return s
-def generate_rss(request, books):
- book = request.args.get('a') # audiobook hash
-
+def generate_rss(base_url, book, books, static=False):
# we only make use of the itunes ns, others provided for posterity
namespaces = {
'itunes':'http://www.itunes.com/dtds/podcast-1.0.dtd',
@@ -156,8 +154,9 @@ def generate_rss(request, books):
pub_format = '%a, %d %b %Y %H:%M:%S %z'
pub_date.text = (date(2000, 12, 31) - timedelta(days=idx)).strftime(
pub_format)
+ url_format = '{}{}/{}.mp3' if static else '{}?a={}&f={}'
enc_attr = {
- 'url': '{}?a={}&f={}'.format(request.base_url, book, f),
+ 'url': url_format.format(base_url, book, f),
'length': str(books[book]['files'][f]['size_bytes']),
'type': 'audio/mpeg'
}
diff --git a/roka.py b/roka.py
index ebfd9cb..252ade9 100755
--- a/roka.py
+++ b/roka.py
@@ -2,13 +2,19 @@
import argparse
import os
-from flask import Flask, request, Response, render_template, send_file
+import shutil
+import json
+from flask import Flask, request, Response, render_template, send_file, templating
+from flask.globals import _app_ctx_stack
from lib.books import Books
from lib.util import check_auth, escape, generate_rss, read_cache
abs_path = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__)
-app.config.from_pyfile(os.path.join(abs_path, 'app.cfg'))
+config_path = os.path.join(abs_path, 'app.cfg')
+config_exists = os.path.exists(config_path)
+if config_exists or __name__.startswith('uwsgi'):
+ app.config.from_pyfile(config_path)
cache_path = os.path.join(abs_path, 'cache')
json_path = os.path.join(cache_path, 'audiobooks.json')
@@ -40,7 +46,7 @@ def list_books():
if not books.get(a):
return 'book not found', 404
- rss = generate_rss(request, books)
+ rss = generate_rss(request.base_url, a, books)
return Response(rss, mimetype='text/xml')
else:
@@ -52,17 +58,72 @@ def list_books():
return render_template('index.html', books=books,
show_path=app.config.get('SHOW_PATH', True))
+def generate(static_path, base_url, audiobook_dirs):
+ static_index_path = os.path.join(static_path, 'index.html')
+
+ books = Books()
+ books.scan_books(audiobook_dirs)
+ books.write_cache()
+ books = read_cache(json_path)
+ # A bit of a hack, but push to the app context stack so we can render a
+ # template outside of a Flask request
+ _app_ctx_stack.push(app.app_context())
+ index = render_template('index.html', books=books, static=True)
+ _app_ctx_stack.pop()
+
+ os.makedirs(static_path, exist_ok=True)
+
+ indexfile = open(static_index_path, 'w')
+ indexfile.write(index)
+ indexfile.close()
+
+ for b_key, book in books.items():
+ rss = generate_rss(base_url, b_key, books, static=True)
+ rss_path = os.path.join(static_path, b_key + '.xml')
+ rssfile = open(rss_path, 'w')
+ rssfile.write(rss.decode('utf-8'))
+ rssfile.close()
+
+ book_dir = os.path.join(static_path, b_key)
+ os.makedirs(book_dir, exist_ok=True)
+
+ for f_key, file in book['files'].items():
+ f_path = file['path']
+ copy_path = os.path.join(book_dir, f_key + '.mp3')
+ if not os.path.exists(copy_path):
+ shutil.copyfile(f_path, copy_path)
+
if __name__ == '__main__':
desc = 'roka: listen to audiobooks with podcast apps via RSS'
parser = argparse.ArgumentParser(description=desc)
parser.add_argument('--scan', dest='scan', action='store_true',
help='scan audiobooks directory for new books',
required=False)
+ parser.add_argument('--generate', dest='static_path', type=str, action='store',
+ help='Output directory to generate static files',
+ required=False)
+ parser.add_argument('--config', dest='config', type=str, action='store',
+ help='Json configuration instead of app.cfg',
+ required=False)
args = parser.parse_args()
+ if args.config:
+ class objectview(object):
+ def __init__(self, d):
+ self.__dict__ = d
+ config = objectview(json.loads(args.config))
+ # override app.cfg
+ app.config.from_object(config)
+ elif not config_exists:
+ raise Exception(f"Config file '{config_path}' doesn't exist")
+
+ root_path = os.path.expanduser(app.config['ROOT_PATH'])
+
if args.scan:
books = Books()
- books.scan_books()
+ books.scan_books(root_path)
books.write_cache()
+ elif args.static_path:
+ generate(args.static_path, app.config['BASE_URL'], root_path)
else:
app.run(host='127.0.0.1', port='8085', threaded=True)
diff --git a/templates/index.html b/templates/index.html
index a31b763..bac87d2 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -38,7 +38,7 @@
</tr>
{% for b, v in books.items() %}
<tr>
- <td><a href="?a={{ b }}">{{ v['title']|escape }}</a></td>
+ <td><a href="{{'?a=' if not static else '/'}}{{ b }}{{'.xml' if static}}">{{ v['title']|escape }}</a></td>
<td>{{ v['author'] }}</td>
{% if show_path %}
<td>{{ v['path']|escape }}</td>