diff options
| -rw-r--r-- | app.py | 72 | ||||
| -rw-r--r-- | database.py | 78 | ||||
| -rw-r--r-- | parser.py | 114 | ||||
| -rw-r--r-- | requirements.txt | 10 | ||||
| -rw-r--r-- | services.py | 2 | ||||
| -rw-r--r-- | static/images/t30.jpg | bin | 0 -> 235413 bytes | |||
| -rw-r--r-- | static/style.css | 7 | ||||
| -rw-r--r-- | templates/template.html | 16 | ||||
| -rw-r--r-- | templates/thoughts.html | 12 | 
9 files changed, 291 insertions, 20 deletions
| @@ -1,8 +1,13 @@ +from PIL import Image  import configparser  import webbrowser +import datetime  import database  import services +import parser  import flask +import os +import io  app = flask.Flask(__name__)  CONFIG = configparser.ConfigParser() @@ -11,7 +16,7 @@ CONFIG.read("edaweb.conf")  def get_template_items(title, db):      return {          "links": db.get_header_links(), -        "image": db.get_image("twitterpic"), +        "image": db.get_pfp_image(),          "title": title,          "articles": db.get_header_articles()      } @@ -45,14 +50,73 @@ def serve_services():              pihole = services.get_pihole_stats()          ) +@app.route("/thought") +def get_thought(): +    thought_id = flask.request.args.get("id", type=int) +    with database.Database() as db: +        category_name, title, dt, parsed = parser.get_thought_from_id(db, thought_id) +        return flask.render_template_string( +            parsed, +            **get_template_items(title, db), +            thought = True, +            dt = "published: " + str(dt), +            category = category_name, +            othercategories = db.get_categories_not(category_name) +        ) + +@app.route("/thoughts") +def get_thoughts(): +    with database.Database() as db: +        all_ = db.get_all_thoughts() +        tree = {} +        for id_, title, dt, category in all_: +            if category not in tree.keys(): +                tree[category] = [(id_, title, dt)] +            else: +                tree[category].append((id_, title, str(dt))) + +        return flask.render_template( +            "thoughts.html", +            **get_template_items("thoughts", db), +            tree = tree +        ) + +@app.route("/img/<filename>") +def serve_image(filename): +    imdirpath = os.path.join(".", "static", "images") +    if filename in os.listdir(imdirpath): +        try: +            w = int(flask.request.args['w']) +            h = int(flask.request.args['h']) +        except (KeyError, ValueError): +            return flask.send_from_directory(imdirpath, filename) + +        img = Image.open(os.path.join(imdirpath, filename)) +        img.thumbnail((w, h), Image.ANTIALIAS) +        io_ = io.BytesIO() +        img.save(io_, format='JPEG') +        return flask.Response(io_.getvalue(), mimetype='image/jpeg') + + + +    else: +        flask.abort(404) + +  @app.route("/preview")  def preview(): -    import os      if "PREVIEW" in os.environ:          with database.Database() as db: -            return flask.render_template_string(os.environ["PREVIEW"], **get_template_items(os.environ["PREVIEW_TITLE"], db)) +            return flask.render_template_string( +                os.environ["PREVIEW"], +                **get_template_items(os.environ["PREVIEW_TITLE"], db), +                thought = True, +                dt = "preview rendered: " + str(datetime.datetime.now()), +                category = os.environ["CATEGORY"], +                othercategories = db.get_categories_not(os.environ["CATEGORY"]) +            )      else: -        return "page for internal use only" +        flask.abort(404) diff --git a/database.py b/database.py index 93f7538..142d942 100644 --- a/database.py +++ b/database.py @@ -1,12 +1,28 @@ +from dataclasses import dataclass  import pymysql +import random  import app +@dataclass  class Database: +    safeLogin:bool = True   #automatically login with the user in the config file, who is read only +    user:str = None         #otherwise, login with the given username and passwd +    passwd:str = None +      def __enter__(self): -        self.__connection = pymysql.connect( -            **app.CONFIG["mysql"], -            charset = "utf8mb4" -        ) +        if self.safeLogin: +            self.__connection = pymysql.connect( +                **app.CONFIG["mysql"], +                charset = "utf8mb4" +            ) +        else: +            self.__connection = pymysql.connect( +                user = self.user, +                passwd = self.passwd, +                host = app.CONFIG["mysql"]["host"], +                db = app.CONFIG["mysql"]["db"], +                charset = "utf8mb4" +            )          return self      def __exit__(self, type, value, traceback): @@ -22,12 +38,64 @@ class Database:              cursor.execute("SELECT alt, url FROM images WHERE imageName = %s;", (imageName, ))              return cursor.fetchone() +    def get_pfp_image(self): +        with self.__connection.cursor() as cursor: +            cursor.execute("SELECT alt, url FROM images WHERE pfp_img = 1;") +            return random.choice(cursor.fetchall()) +      def get_header_articles(self):          with self.__connection.cursor() as cursor:              cursor.execute("SELECT articleName, link FROM headerArticles;")              return cursor.fetchall() +    def get_all_categories(self): +        with self.__connection.cursor() as cursor: +            cursor.execute("SELECT category_name FROM categories;") +            return [i[0] for i in cursor.fetchall()] + +    def add_category(self, category): +        if not category in self.get_all_categories(): +            with self.__connection.cursor() as cursor: +                cursor.execute("INSERT INTO categories (category_name) VALUES (%s);", (category, )) + +            self.__connection.commit() +            return True +             +        return False + +    def add_thought(self, category, title, markdown): +        with self.__connection.cursor() as cursor: +            cursor.execute(""" +            INSERT INTO thoughts (category_id, title, markdown_text)  +            VALUES (( +                SELECT category_id FROM categories WHERE category_name = %s +            ), %s, %s);""", (category, title, markdown)) +        self.__connection.commit() + +    def get_thought(self, id_): +        with self.__connection.cursor() as cursor: +            cursor.execute(""" +            SELECT categories.category_name, thoughts.title, thoughts.dt, thoughts.markdown_text  +            FROM thoughts INNER JOIN categories  +            ON thoughts.category_id = categories.category_id  +            WHERE thought_id = %s;""", (id_, )) +            return cursor.fetchone() + +    def get_categories_not(self, category_name): +        with self.__connection.cursor() as cursor: +            cursor.execute("SELECT category_name FROM categories WHERE category_name != %s;", (category_name, )) +            return [i[0] for i in cursor.fetchall()] + +    def get_all_thoughts(self): +        with self.__connection.cursor() as cursor: +            cursor.execute(""" +            SELECT thought_id, title, dt, category_name FROM thoughts  +            INNER JOIN categories ON thoughts.category_id = categories.category_id; +            """) +            return cursor.fetchall() + +  if __name__ == "__main__":      with Database() as db: -        print(db.get_image("headerImage"))
\ No newline at end of file +        print(db.get_all_thoughts())
\ No newline at end of file @@ -1,6 +1,8 @@  import argparse  from urllib.parse import urlparse  import webbrowser +import database +import getpass  import app  import re  import os @@ -8,13 +10,22 @@ import os  HEADER_INCREMENTER = 1  IMAGE_TYPES = [".png", ".jpg"] +def get_thought_from_id(db, id_): +    category_name, title, dt, markdown = db.get_thought(id_) +    return category_name, title, dt, parse_text(markdown) +  def parse_file(path):      with open(path, "r") as f:          unformatted = f.read() +    return parse_text(unformatted)     + +def parse_text(unformatted):      formatted = parse_headers(unformatted)      formatted = parse_asteriscs(formatted)      formatted = parse_links(formatted) +    formatted = parse_code(formatted) +    formatted = parse_lists(formatted)      formatted = add_linebreaks(formatted)      return '{% extends "template.html" %}\n{% block content %}\n' + formatted + '\n{% endblock %}' @@ -77,17 +88,71 @@ def parse_links(test_str):      return test_str +def parse_code(test_str): +    regex = r"(?<!\\)`\w{1,}?`" +    # this only matches single words, but escaping is less complicated +    matches = re.finditer(regex, test_str, re.MULTILINE) +    offset = 0 + +    for match in matches: +        replacement = "<em class=inlineCode style='font-family: monospace;font-style: normal;'>%s</em>" % match.group()[1:-1] +        test_str = test_str[:match.start()+offset] + replacement + test_str[match.end()+offset:] +        offset += (len(replacement) - (match.end() - match.start())) + +    out = "" +    inBlock = 0 +    for line in test_str.split("\n"): +        if line == "```": +            if inBlock % 2 == 0: +                out += "<p class=codeBlock style='font-family: monospace;font-style: normal;white-space: pre-wrap;'>\n" +            else: +                out += "</p>\n" +            inBlock += 1 +        else: +            out += line + "\n" + +    return out + +def parse_lists(test_str): +    regex = r"^[1-9][.)] .*$|- .*$" +    matches = re.finditer(regex, test_str, re.MULTILINE) +    offset = 0 +    theFirstOne = True + +    for match in matches: +        if theFirstOne: +            if match.group()[0].isdigit(): +                listType = "ol" +                cutoff = 3 +            else: +                listType = "ul" +                cutoff = 2 +            replacement = "<%s>\n<li>%s</li>" % (listType, match.group()[cutoff:]) +            theFirstOne = False +        else: +            if re.match(regex, [i for i in test_str[match.end()+offset:].split("\n") if i != ''][0]) is None: +                theFirstOne = True +                replacement = "<li>%s</li>\n</%s>" % (match.group()[cutoff:], listType) +            else: +                replacement = "<li>%s</li>" % match.group()[cutoff:] +        test_str = test_str[:match.start()+offset] + replacement + test_str[match.end()+offset:] +        offset += (len(replacement) - (match.end() - match.start())) + +    return test_str +  def add_linebreaks(test_str):      return re.sub(r"^$", "<br><br>", test_str, 0, re.MULTILINE) -def preview_markdown(path, title): +def preview_markdown(path, title, category):      def startBrowser():          webbrowser.get("firefox").open("http://localhost:5000/preview")          del os.environ["PREVIEW"]          del os.environ["PREVIEW_TITLE"] +        del os.environ["CATEGORY"]      os.environ["PREVIEW"] = parse_file(path)      os.environ["PREVIEW_TITLE"] = title +    os.environ["CATEGORY"] = category      import threading      threading.Timer(1.25, startBrowser ).start() @@ -97,8 +162,13 @@ def preview_markdown(path, title):  def main():      p = argparse.ArgumentParser()      p.add_argument( -        "-m", "--markdown", +        "markdown",          help = "Path to a markdown file", +        type = str +    ) +    p.add_argument( +        "-u", "--username", +        help = "Username to use for the database",          required = True,          type = str      ) @@ -109,15 +179,47 @@ def main():          type = str      )      p.add_argument( +        "-c", "--category", +        help = "Article category", +        required = True, +        type = str +    ) +    g = p.add_mutually_exclusive_group(required = True) +    g.add_argument(          "-p", "--preview",          help = "Preview markdown rendering",          action='store_true'      ) +    g.add_argument( +        "-s", "--save", +        help = "Save markdown to database", +        action='store_true' +    ) +    g.add_argument( +        "-e", "--echo", +        help = "Print parsed markdown to stdout", +        action='store_true' +    )      args = vars(p.parse_args()) -    if args["preview"]: -        preview_markdown(args["markdown"], args["title"]) -    else: -        print(parse_file(args["markdown"])) + +    passwd = getpass.getpass("Enter password for %s@%s: " % (args["username"], app.CONFIG["mysql"]["host"])) + +    with database.Database( +        safeLogin = False, +        user = args["username"], +        passwd = passwd +    ) as db: + +        if args["preview"]: +            preview_markdown(args["markdown"], args["title"], args["category"]) +        elif args["save"]: +            if db.add_category(args["category"]): +                print("Added category...") +            with open(args["markdown"], "r") as f: +                db.add_thought(args["category"], args["title"], f.read()) +            print("Added thought...") +        else: +            print(parse_file(args["markdown"]))  if __name__ == "__main__":      main()
\ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e86483b..fd89b0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,10 @@ -python_qbittorrent==0.4.2  PyMySQL==1.0.2 +python_qbittorrent==0.4.2  Flask==1.1.2 -PiHole_api==2.6 -clutch==0.1a2 -docker_py==1.10.6 +PiHole_api +transmission-clutch +dataclasses +docker  pihole==0.1.2 +Pillow==8.1.0  qbittorrent==0.1.6 diff --git a/services.py b/services.py index b592427..32edb5d 100644 --- a/services.py +++ b/services.py @@ -33,7 +33,7 @@ def timeout(func):      # this works but Manager() uses an extra thread than Queue()      manager = multiprocessing.Manager()      returnVan = manager.list() -    ti = time.time() +    # ti = time.time()      def runFunc(q, func):          q.append(func()) diff --git a/static/images/t30.jpg b/static/images/t30.jpgBinary files differ new file mode 100644 index 0000000..949d14b --- /dev/null +++ b/static/images/t30.jpg diff --git a/static/style.css b/static/style.css index 7bc2190..7a0bf68 100644 --- a/static/style.css +++ b/static/style.css @@ -90,6 +90,13 @@ article section table td {      text-align: right;  } +aside { +    width: 30%; +    padding-left: 15px; +    margin-left: 15px; +    float: right; +} +  .running {      background-color: green;      padding: 1, 1, 1, 1; diff --git a/templates/template.html b/templates/template.html index 6b0d894..afbeefe 100644 --- a/templates/template.html +++ b/templates/template.html @@ -36,6 +36,22 @@                      </a>                           </div>              </header> +            <!-- yes, this is stupid asf, using another template would be better (i cant get that to work) --> +            {% if thought %} +                <aside> +                    <h4>{{dt}}</h4> +                    <h5>this category:</h5> +                    <ul> +                        <li><b><a href={{'/thoughts#'+category.replace(' ', '_')}}>{{category}}</a></b></li> +                    </ul> +                    <h5>other categories:</h5> +                    {% for category_name in othercategories %} +                        <ul> +                            <li><a href={{'/thoughts#'+category_name.replace(' ', '_')}}>{{category_name}}</a></li> +                        </ul> +                    {% endfor %} +                </aside> +            {% endif %}              <article>                  {% block content %}                  {% endblock %} diff --git a/templates/thoughts.html b/templates/thoughts.html new file mode 100644 index 0000000..8717299 --- /dev/null +++ b/templates/thoughts.html @@ -0,0 +1,12 @@ +{% extends "template.html" %} +{% block content %} +    {% for category_name, thoughts in tree.items() %} +    <h2 id={{category_name.replace(' ', '_')}}>{{category_name}}</h2> +    <dl> +        {% for id_, title, dt in thoughts %} +        <dt><a href={{'/thought?id=%i' % id_}}>title</a></dt> +        <dd>{{dt}}</dd> +        {% endfor %} +    </dl> +    {% endfor %} +{% endblock %}
\ No newline at end of file | 
