Ver Fonte

initial commit of subtitle webapp

Breandan Dezendorf há 2 anos atrás
pai
commit
8be4bc05f0

+ 9 - 0
dezendorf/applications/subtitles/Dockerfile

@@ -0,0 +1,9 @@
+FROM python:3
+
+ADD subtitles.py /
+ADD requirements.txt /
+ADD templates /templates
+
+RUN pip3 install -r requirements.txt
+
+ENTRYPOINT ["python3", "subtitles.py"]

+ 2 - 0
dezendorf/applications/subtitles/Makefile

@@ -0,0 +1,2 @@
+migrate:
+	FLASK_APP=subtitles.py python3 -m  flask db upgrade

BIN
dezendorf/applications/subtitles/__pycache__/subtitles.cpython-39.pyc


+ 15 - 0
dezendorf/applications/subtitles/build.sh

@@ -0,0 +1,15 @@
+TAG=$(git rev-parse --short HEAD)
+NAME="docker.dezendorf.net/subtitles"
+YAML="../../homelab/k3s/subtitles/subtitles.yaml"
+docker build --no-cache -t ${NAME}:${TAG} . < Dockerfile
+docker push ${NAME}:${TAG}
+docker push ${NAME}:latest
+
+echo "Built and pushed:"
+echo "  ${NAME}:${TAG}"
+echo "  ${NAME}:latest"
+
+sed -i -e "s#$NAME:.*#$NAME:$TAG#g" ${YAML}
+git add ${YAML}
+
+

+ 1 - 0
dezendorf/applications/subtitles/migrations/README

@@ -0,0 +1 @@
+Single-database configuration for Flask.

+ 50 - 0
dezendorf/applications/subtitles/migrations/alembic.ini

@@ -0,0 +1,50 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic,flask_migrate
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S

+ 110 - 0
dezendorf/applications/subtitles/migrations/env.py

@@ -0,0 +1,110 @@
+import logging
+from logging.config import fileConfig
+
+from flask import current_app
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+
+def get_engine():
+    try:
+        # this works with Flask-SQLAlchemy<3 and Alchemical
+        return current_app.extensions['migrate'].db.get_engine()
+    except TypeError:
+        # this works with Flask-SQLAlchemy>=3
+        return current_app.extensions['migrate'].db.engine
+
+
+def get_engine_url():
+    try:
+        return get_engine().url.render_as_string(hide_password=False).replace(
+            '%', '%%')
+    except AttributeError:
+        return str(get_engine().url).replace('%', '%%')
+
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+config.set_main_option('sqlalchemy.url', get_engine_url())
+target_db = current_app.extensions['migrate'].db
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def get_metadata():
+    if hasattr(target_db, 'metadatas'):
+        return target_db.metadatas[None]
+    return target_db.metadata
+
+
+def run_migrations_offline():
+    """Run migrations in 'offline' mode.
+
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+
+    Calls to context.execute() here emit the given string to the
+    script output.
+
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(
+        url=url, target_metadata=get_metadata(), literal_binds=True
+    )
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online():
+    """Run migrations in 'online' mode.
+
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+
+    """
+
+    # this callback is used to prevent an auto-migration from being generated
+    # when there are no changes to the schema
+    # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
+    def process_revision_directives(context, revision, directives):
+        if getattr(config.cmd_opts, 'autogenerate', False):
+            script = directives[0]
+            if script.upgrade_ops.is_empty():
+                directives[:] = []
+                logger.info('No changes in schema detected.')
+
+    connectable = get_engine()
+
+    with connectable.connect() as connection:
+        context.configure(
+            connection=connection,
+            target_metadata=get_metadata(),
+            process_revision_directives=process_revision_directives,
+            **current_app.extensions['migrate'].configure_args
+        )
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()

+ 24 - 0
dezendorf/applications/subtitles/migrations/script.py.mako

@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+    ${downgrades if downgrades else "pass"}

+ 7 - 0
dezendorf/applications/subtitles/requirements.txt

@@ -0,0 +1,7 @@
+
+Flask-SQLAlchemy
+flask-Marshmallow
+Flask-Migrate
+mysqlclient
+marshmallow-sqlalchemy
+watchdog

+ 136 - 0
dezendorf/applications/subtitles/subtitles.py

@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+
+import flask
+
+from flask import make_response, request, redirect, jsonify, render_template, url_for
+from flask_sqlalchemy import SQLAlchemy
+from flask_marshmallow import Marshmallow
+from flask_migrate import Migrate
+
+db = SQLAlchemy()
+app = flask.Flask(__name__)
+app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:m9Eb5OflQb@mysql.mysql.svc.cluster.local/subtitles'
+db.init_app(app)
+
+migrate = Migrate(app, db)
+
+ma = Marshmallow(app)
+
+class SubtitleEntry(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    movie_id = db.Column(db.String(255), db.ForeignKey('movie.id'))
+    text = db.Column(db.String(255))
+    lineNumber = db.Column(db.Integer)
+    startTime = db.Column(db.String(255))
+    endTime = db.Column(db.String(255))
+
+
+class SubtitleEntrySchema(ma.Schema):
+    class Meta:
+        fields = ('id', 'movie_id', 'text', 'lineNumber', 'startTime', 'endTime')
+        
+class Movie(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    movieName = db.Column(db.String(255))
+    releaseYear = db.Column(db.String(255))
+    srtPath = db.Column(db.String(255))
+    moviePath = db.Column(db.String(255))
+    runLength = db.Column(db.Integer)
+    lastModified = db.Column(db.String(255))
+
+class MovieSchema(ma.Schema):
+    class Meta:
+        fields = ('id', 'movieName', 'releaseYear', 'srtPath', 'moviePath', 'runLength', 'lastModified')
+
+def get_links():
+    links = Link.query.order_by(Link.name).all()
+    schema = LinkSchema(many=True)
+    link_json = schema.dump(links)
+    return links
+
+def link_exists(link_name):
+    try:
+        link = Link.query.filter_by(name=link_name).first()
+    except Exception as e:
+        print(e)
+        return e
+    if link is None:
+        return False
+    else:
+        return True
+
+
+@app.route('/<string:name>', strict_slashes=False, methods=['GET'])
+def redirect_to_link(name):
+    try:
+        link = Link.query.filter_by(name=name).first()
+    except e:
+        return redirect("/", code=302)
+    if link is None:
+        return redirect("/", code=302)
+    try:
+        link.hit_count += 1
+    except TypeError:
+        link.hit_count = 1
+    db.session.commit()
+    print("updating link hit count")
+    return redirect(link.target, code=302)
+
+
+@app.post('/add', strict_slashes=False)
+def add_link():
+    if link_exists(request.form['link_name']) is True:
+        print("Link exists")
+    else:
+        print("Creating link")
+        db.create_all()
+        link = Link(name=request.form['link_name'], target=request.form['target'], hit_count=0, owner_name="unknown")
+        db.session.add(link)
+        db.session.commit()
+    return redirect("/", code=302)
+
+
+@app.route('/<string:link_name>/add', strict_slashes=False)
+def add_link_form(link_name):
+    return render_template('add.html', link_name=link_name)
+
+
+@app.post('/<string:link_name>/edit', strict_slashes=False)
+def edit_link(link_name):
+    db.create_all()
+    link = Link.query.filter_by(name=link_name).first()
+    print("Setting link target to {}".format(request.form['target']))
+    link.target = request.form['target']
+    db.session.commit()
+    return redirect("/", code=302)
+
+
+@app.route('/<string:link_name>/edit', strict_slashes=False)
+def edit_link_form(link_name):
+    link = Link.query.filter_by(name=link_name).first()
+    return render_template('edit.html', link=link)
+
+
+@app.post('/<int:link_id>/delete', strict_slashes=False)
+def delete_link(link_id):
+    link = Link.query.filter_by(id=link_id).first()
+    db.session.delete(link)
+    db.session.commit()
+    return redirect(url_for('list_links'))
+
+
+@app.route('/<int:link_id>/delete', strict_slashes=False)
+def delete_link_form(link_id):
+    link = Link.query.filter_by(id=link_id).first()
+    return render_template('delete.html', link=link)
+
+
+@app.route('/')
+def list_links():
+    links = get_links()
+    return render_template('list.html', links=links)
+
+#@app.route('/links/<int:id>', methods=['GET'])
+
+if __name__ == "__main__":
+    app.run(host='0.0.0.0')

+ 21 - 0
dezendorf/applications/subtitles/templates/add.html

@@ -0,0 +1,21 @@
+{% extends 'base.html' %}
+
+{% block content %}
+    <h1>{% block title %} Add a New Link {% endblock %}</h1>
+    <form method="post">
+        <label for="title">{{ link_name }}</label>
+        <br>
+        {{ request.form['link_name'] }}
+        <input type="hidden" name="link_name"
+               placeholder="Link name"
+               value="{{ request.form['link_name'] }}"></input>
+        <br>
+        <label for="content">Target</label>
+        <br>
+        <input type="text" name="target"
+               placeholder="http://example.com"
+               value="{{ request.form['target'] }}"></input>
+        <br>
+        <button type="submit">Submit</button>
+    </form>
+{% endblock %}

+ 17 - 0
dezendorf/applications/subtitles/templates/base.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
+   
+    <title>{% block title %} {% endblock %} - FlaskApp</title>
+</head>
+<body>
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
+    <div class="content">
+        {% block content %} {% endblock %}
+    </div>
+</body>
+</html>

+ 18 - 0
dezendorf/applications/subtitles/templates/delete.html

@@ -0,0 +1,18 @@
+{% extends 'base.html' %}
+
+{% block content %}
+    <h1>{% block title %} Delete Link{% endblock %}</h1>
+    <form method="post">
+        <div class='link'>
+            <div>
+                <span class="name">{{ link['name'] }}</span>
+                <span class="target">{{ link['target'] }}</span>
+                <span class="delete_button"><b>Delete</b></span>
+                <span class="edit_button">Edit</span>
+            </div>
+        </div>
+        <input type="hidden" name="link_id" value="{{ link['name'] }}">
+        <br>
+        <button type="submit">Submit</button>
+    </form>
+{% endblock %}

+ 21 - 0
dezendorf/applications/subtitles/templates/edit.html

@@ -0,0 +1,21 @@
+{% extends 'base.html' %}
+
+{% block content %}
+<div class="container">
+    <form method="post">
+    <div class="mb-3">
+        <label for="formNameInput" class="form-label">Name</label>
+        <input type="text" readonly class="form-control-plaintext" id="formNameInput" name="link_name" placeholder="{{ link.name }}">
+      </div>
+      <div class="mb-3">
+        <label for="formTargetInput" class="form-label">Target</label>
+        <input type="text" class="form-control" id="formTargetInput" name="target" value="{{ link.target }}">
+      </div>
+      <div class="mb-3">
+        <button type="submit">Submit</button>
+        </div>
+    </form>
+</div>
+</div>
+
+{% endblock %}

+ 62 - 0
dezendorf/applications/subtitles/templates/list.html

@@ -0,0 +1,62 @@
+{% extends 'base.html' %}
+
+{% block content %}
+<div class="container">
+  <form method="post" action="{{ url_for('add_link', link_name='') }}">
+  <div class="row">
+    <div class="col-sm-2">
+        <input type="text" name="link_name" placeholer="name"></input>
+    </div>
+    <div class="col-sm-8">
+        <input type="text" name="target" placeholder="http://example.com"></input>
+    </div>
+    <div class="col-sm-1">
+        <button type="submit">Add</button>
+    </div>
+  </div>
+  </form>
+  {% for link in links %}
+  <div class="row gx-5 border">
+    <div class="col-sm-2" >
+        <div class="border bg-light">
+            <a href="/{{ link['name'] }}">{{ link['name'] }}</a>
+        </div>
+    </div>
+    <div class="col-sm-8">
+        {{ link['target'] }}
+    </div>
+    <div class="col-sm-1">
+        <a class="btn btn-primary" data-bs-toggle="collapse" href="#linkCollapse{{ link['id'] }}" role="button" aria-expanded="false" aria-controls="linkCollapse{{ link['id'] }}">
+          Details
+        </a>
+    </div>
+    <div class="collapse" id="linkCollapse{{ link['id'] }}">
+      <div class="card card-body">
+        <div>
+            <div class="container">
+                <form method="post" action="{{ url_for('edit_link', link_name=link.name) }}">
+                <div class="mb-3">
+                    <label for="formNameInput" class="form-label">Name</label>
+                    <input type="text" readonly class="form-control-plaintext" id="formNameInput" name="link_name" placeholder="{{ link.name }}">
+                  </div>
+                  <div class="mb-3">
+                    <label for="formTargetInput" class="form-label">Target</label>
+                    <input type="text" class="form-control" id="formTargetInput" name="target" value="{{ link.target }}">
+                  </div>
+                  <div class="mb-3">
+                    <button type="submit">Update</button>
+                    </form>
+                    <form method="post" action="{{ url_for('delete_link', link_id=link.id) }}">
+  
+                    <button type="submit">Delete</button>
+                    
+                    </form>
+                    </div>
+            </div>
+        </div>
+      </div>
+    </div>
+  </div>
+    {% endfor %}
+</div>
+{% endblock %}

+ 0 - 0
dezendorf/applications/subtitles/templates/search.html