first commit (create a local server)
This commit is contained in:
commit
2a4201d68f
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
env
|
||||
.env
|
||||
movielist.sqlite
|
||||
posters/*
|
||||
__pycache__
|
||||
*~
|
55
README.md
Normal file
55
README.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Things you'll have to do once to initialize the project
|
||||
Do the following in order:
|
||||
## Create an .env file
|
||||
```(bash)
|
||||
cp env.example .env
|
||||
```
|
||||
Then edit the .env file to enter a flask secret key, your omdb key and an administrator password for the site.
|
||||
|
||||
## Initialize the environment
|
||||
There is a configure.sh file:
|
||||
```(bash)
|
||||
sh configure.sh
|
||||
```
|
||||
You can also initialize the environment yourself:
|
||||
|
||||
### Create a virtual environment
|
||||
Create an `env` folder:
|
||||
```(bash)
|
||||
python -m venv env
|
||||
```
|
||||
To enter the `(env)` environment:
|
||||
```(bash)
|
||||
source env/bin/activate
|
||||
```
|
||||
The keyword `(env)` should appear in the beginning of the prompt. If you want to quit the virtual environment, do `deactivate`.
|
||||
If you are not in the virtual environment (the `(env)` keyword does not appear in the prompt), do `source env/bin/activate` again to go back to the environment.
|
||||
|
||||
### Install the required python packages
|
||||
```(bash)
|
||||
python -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Initialize the database
|
||||
Warning! This will erase the database.
|
||||
Pro-tip: this will erase the database.
|
||||
```(bash)
|
||||
python -m flask --app kinolist init-db
|
||||
```
|
||||
|
||||
# Things to do whenever you want to launch the project locally
|
||||
## Enter the local environment
|
||||
```(bash)
|
||||
source env/bin/activate
|
||||
```
|
||||
The keyword `(env)` should appear in the beginning of the prompt. If you want to quit the virtual environment, do `deactivate`.
|
||||
If you are not in the virtual environment (the `(env)` keyword does not appear in the prompt), do `source env/bin/activate` again to go back to the environment.
|
||||
|
||||
## Launch the Flask server
|
||||
```(bash)
|
||||
python -m flask --app kinolist run --port 8000 --debug
|
||||
```
|
||||
You can access the site at the adress `localhost:8000` on your web browser. Do `Ctrl+C` to stop the server.
|
||||
|
||||
# Remarks
|
||||
For some reasons, I need a symbolic link from `posters/` to `kinolist/posters/`, I guess it is because python keeps changing the base directory, so the fix I have found is to have two `posters` folders which are actually the same.
|
10
configure.sh
Normal file
10
configure.sh
Normal file
@ -0,0 +1,10 @@
|
||||
if [ ! -d env ]; then
|
||||
python -m venv env
|
||||
source env/bin/activate
|
||||
python -m pip install -r requirements.txt
|
||||
fi
|
||||
|
||||
if [ ! -f movielist.sqlite ]; then
|
||||
source env/bin/activate
|
||||
python -m flask --app kinolist init-db
|
||||
fi
|
6
env.example
Normal file
6
env.example
Normal file
@ -0,0 +1,6 @@
|
||||
FLASK_SECRET_KEY="PUT YOUR SECRET KEY HERE"
|
||||
FLASK_OMDB_KEY="PUT YOUR OMDB KEY HERE"
|
||||
FLASK_ADMIN_PASSWORD="PUT THE ADMIN PASSWORD HERE"
|
||||
FLASK_DATABASE=movielist.sqlite
|
||||
FLASK_UPLOAD_FOLDER=posters
|
||||
FLASK_IMAGE_WIDTH=200
|
16
kinolist/__init__.py
Normal file
16
kinolist/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from flask import Flask
|
||||
|
||||
from kinolist import pages, database, login
|
||||
|
||||
load_dotenv()
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.config.from_prefixed_env()
|
||||
login.login_manager.init_app(app)
|
||||
database.init_app(app)
|
||||
|
||||
app.register_blueprint(pages.bp)
|
||||
return app
|
106
kinolist/database.py
Normal file
106
kinolist/database.py
Normal file
@ -0,0 +1,106 @@
|
||||
import sqlite3
|
||||
import click
|
||||
from flask import current_app, g
|
||||
from os import getenv
|
||||
|
||||
def init_app(app):
|
||||
app.teardown_appcontext(close_db)
|
||||
app.cli.add_command(init_db_command)
|
||||
|
||||
@click.command("init-db")
|
||||
def init_db_command():
|
||||
db = get_db()
|
||||
with current_app.open_resource("schema.sql") as f:
|
||||
db.executescript(f.read().decode("utf-8"))
|
||||
click.echo("You successfully initialized the database.")
|
||||
|
||||
def get_db():
|
||||
try:
|
||||
return g.db
|
||||
except:
|
||||
g.db = sqlite3.connect(
|
||||
current_app.config["DATABASE"],
|
||||
detect_types=sqlite3.PARSE_DECLTYPES
|
||||
)
|
||||
g.db.row_factory = sqlite3.Row
|
||||
return g.db
|
||||
|
||||
def close_db(e=None):
|
||||
db = g.pop("db", None)
|
||||
if db is not None:
|
||||
db.close()
|
||||
|
||||
def new_nb():
|
||||
res = get_db().execute(
|
||||
"SELECT number FROM movies ORDER BY number DESC"
|
||||
).fetchone()
|
||||
return 1 if res == None else 1 if res["number"] == None else res["number"] + 1
|
||||
|
||||
def move_movie(dest, src):
|
||||
db = get_db()
|
||||
if dest == "end":
|
||||
num = new_nb()
|
||||
else:
|
||||
num = int(db.execute("SELECT number from movies WHERE id = ?", (dest, )).fetchone()["number"])
|
||||
movies = db.execute("SELECT id FROM movies WHERE number >= ? ORDER BY number", (num, )).fetchall()
|
||||
nb = num + 1
|
||||
for movie in movies:
|
||||
db.execute("UPDATE movies SET number = ? WHERE id = ?", (nb, movie["id"]))
|
||||
nb += 1
|
||||
db.execute("UPDATE movies SET number = ? WHERE id = ?", (num, src))
|
||||
db.commit()
|
||||
|
||||
def get_movies():
|
||||
return get_db().execute("SELECT id, title, year, poster, plot, watched, watchDate, score FROM movies ORDER BY number").fetchall()
|
||||
|
||||
def get_movies_to_watch():
|
||||
return get_db().execute("SELECT id, title, year, poster, plot, watched, watchDate, score FROM movies WHERE watched = 0 ORDER BY number").fetchall()
|
||||
|
||||
def get_watched_movies():
|
||||
return get_db().execute("SELECT id, title, year, poster, plot, watched, watchDate, score FROM movies WHERE watched = 1 ORDER BY watchDate DESC").fetchall()
|
||||
|
||||
def new_movie(movie):
|
||||
db = get_db()
|
||||
movie["id"] = db.execute("INSERT INTO movies (title, year, poster, plot, number) VALUES (?,?,?,?,?) RETURNING id",
|
||||
(movie["title"], movie["year"], movie["poster"], movie["plot"], new_nb())).fetchone()["id"]
|
||||
db.commit()
|
||||
return movie
|
||||
|
||||
def new_blank_movie():
|
||||
db = get_db()
|
||||
id = db.execute("INSERT INTO movies DEFAULT VALUES RETURNING id").fetchone()["id"]
|
||||
nb = new_nb()
|
||||
db.execute("UPDATE movies SET number = ? WHERE id = ?", (nb, id))
|
||||
db.commit()
|
||||
return {"id": id, "nb": nb}
|
||||
|
||||
def delete_movie(id):
|
||||
db = get_db()
|
||||
db.execute("DELETE FROM movies WHERE id = ?", (id, ))
|
||||
db.commit()
|
||||
|
||||
def update(id, field, value):
|
||||
db = get_db()
|
||||
db.execute("UPDATE movies SET " + field + " = ? WHERE id = ?", (value, id))
|
||||
db.commit()
|
||||
|
||||
def update_title(id,title):
|
||||
update(id, "title", title)
|
||||
|
||||
def update_year(id,year):
|
||||
update(id, "year", year)
|
||||
|
||||
def update_poster(id,poster):
|
||||
update(id, "poster", poster)
|
||||
|
||||
def update_plot(id, plot):
|
||||
update(id, "plot", plot)
|
||||
|
||||
def update_watched(id, watched):
|
||||
update(id, "watched", watched)
|
||||
|
||||
def update_watchDate(id, watchDate):
|
||||
update(id, "watchDate", watchDate)
|
||||
|
||||
def update_score(id, score):
|
||||
update(id, "score", score)
|
36
kinolist/imdb.py
Normal file
36
kinolist/imdb.py
Normal file
@ -0,0 +1,36 @@
|
||||
from flask import current_app
|
||||
from requests import get
|
||||
from kinolist import posters
|
||||
|
||||
base_url = "http://www.omdbapi.com/"
|
||||
def params(k,v):
|
||||
return {
|
||||
"apikey": current_app.config["OMDB_KEY"],
|
||||
k: v,
|
||||
"type": "movie"
|
||||
}
|
||||
|
||||
def imdbid(imdbid):
|
||||
data = get(base_url, params=params("i", imdbid)).json()
|
||||
if data['Response'] == 'True':
|
||||
movie = {
|
||||
"title": data["Title"],
|
||||
"year": data["Year"],
|
||||
"language": data["Language"],
|
||||
"plot": data["Plot"]
|
||||
}
|
||||
poster = data["Poster"]
|
||||
if poster != "N/A":
|
||||
img = get(poster)
|
||||
if img.status_code == 200:
|
||||
movie["poster"] = posters.save(poster, img.content)
|
||||
return movie
|
||||
else:
|
||||
return None
|
||||
|
||||
def title(name):
|
||||
data = get(base_url, params=params("s", name)).json()
|
||||
if data["Response"] == "True":
|
||||
return data["Search"]
|
||||
else:
|
||||
return None
|
20
kinolist/login.py
Normal file
20
kinolist/login.py
Normal file
@ -0,0 +1,20 @@
|
||||
import flask
|
||||
from flask_login import (LoginManager, UserMixin)
|
||||
|
||||
class User(UserMixin):
|
||||
def __init__(self, id):
|
||||
super().__init__()
|
||||
self.id = id
|
||||
|
||||
login_manager = LoginManager()
|
||||
|
||||
@login_manager.user_loader
|
||||
def user_loader(id):
|
||||
if id == "admin":
|
||||
user = User(id)
|
||||
return user
|
||||
return
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def redirect():
|
||||
return flask.redirect(flask.url_for("pages.login_page"))
|
109
kinolist/pages.py
Normal file
109
kinolist/pages.py
Normal file
@ -0,0 +1,109 @@
|
||||
from flask import (
|
||||
Blueprint,
|
||||
current_app,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for
|
||||
)
|
||||
from flask_login import (
|
||||
login_required,
|
||||
login_user,
|
||||
logout_user
|
||||
)
|
||||
|
||||
from kinolist import (imdb, posters)
|
||||
from kinolist.login import User
|
||||
from kinolist.database import (
|
||||
delete_movie,
|
||||
get_movies,
|
||||
get_movies_to_watch,
|
||||
get_watched_movies,
|
||||
move_movie,
|
||||
new_blank_movie,
|
||||
new_movie,
|
||||
update_plot,
|
||||
update_poster,
|
||||
update_score,
|
||||
update_title,
|
||||
update_watchDate,
|
||||
update_watched,
|
||||
update_year
|
||||
)
|
||||
|
||||
bp = Blueprint("pages", __name__)
|
||||
|
||||
def w():
|
||||
return current_app.config["IMAGE_WIDTH"]
|
||||
|
||||
@bp.route("/")
|
||||
def to_watch():
|
||||
return render_template("pages/to_watch.html", movies=get_movies_to_watch(), to_watch=1, w=w())
|
||||
|
||||
@bp.route("/watched")
|
||||
def watched():
|
||||
return render_template("pages/watched.html", movies=get_watched_movies(), watched=1, w=w())
|
||||
|
||||
@bp.route("/manage", methods=("GET", "POST"))
|
||||
@login_required
|
||||
def manage():
|
||||
if request.method == "POST":
|
||||
if "new" in request.form:
|
||||
choice = request.form["choice"]
|
||||
if choice == "title":
|
||||
movies = imdb.title(request.form["title"])
|
||||
if movies:
|
||||
return render_template("_choicelist.html", movies=movies, w=w())
|
||||
else:
|
||||
return ""
|
||||
elif choice == "imdbid":
|
||||
movie = imdb.imdbid(request.form["imdbid"])
|
||||
if movie:
|
||||
return render_template("_line.html", movie=new_movie(movie), manage=1, w=w())
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
return render_template("_line.html", movie=new_blank_movie(), manage=1, w=w())
|
||||
else:
|
||||
id = request.form["id"]
|
||||
if "move" in request.form:
|
||||
move_movie(id, request.form["move"])
|
||||
if "title" in request.form:
|
||||
update_title(id, request.form["title"])
|
||||
elif "year" in request.form:
|
||||
update_year(id, request.form["year"])
|
||||
elif "poster" in request.files:
|
||||
poster = request.files["poster"]
|
||||
filename = posters.save(poster.filename, poster.read())
|
||||
update_poster(id, filename)
|
||||
return filename;
|
||||
elif "plot" in request.form:
|
||||
update_plot(id, request.form["plot"])
|
||||
elif "watched" in request.form:
|
||||
update_watched(id, request.form["watched"])
|
||||
elif "watchDate" in request.form:
|
||||
update_watchDate(id, request.form["watchDate"])
|
||||
elif "score" in request.form:
|
||||
update_score(id, request.form["score"])
|
||||
elif "delete" in request.form:
|
||||
delete_movie(id)
|
||||
return ""
|
||||
return render_template("pages/manage.html", movies=get_movies(), manage=1, w=w())
|
||||
|
||||
@bp.route("/posters/<filename>")
|
||||
def send_uploaded_file(filename):
|
||||
return posters.get(filename)
|
||||
|
||||
@bp.route("/login", methods=("GET", "POST"))
|
||||
def login_page():
|
||||
if request.method == "POST":
|
||||
if request.form["password"] == current_app.config["ADMIN_PASSWORD"]:
|
||||
login_user(User("admin"))
|
||||
return redirect(url_for("pages.manage"))
|
||||
return "Bad Login"
|
||||
return render_template("pages/login.html")
|
||||
|
||||
@bp.route('/logout')
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for("pages.to_watch"))
|
1
kinolist/posters
Symbolic link
1
kinolist/posters
Symbolic link
@ -0,0 +1 @@
|
||||
../posters
|
15
kinolist/posters.py
Normal file
15
kinolist/posters.py
Normal file
@ -0,0 +1,15 @@
|
||||
from flask import (current_app, send_from_directory)
|
||||
from hashlib import sha1
|
||||
from pathlib import Path
|
||||
from os import path
|
||||
|
||||
def save(name, content):
|
||||
h = sha1(content).hexdigest()
|
||||
filename = h + Path(name).suffix
|
||||
file = open(path.join(current_app.config['UPLOAD_FOLDER'], filename), "wb")
|
||||
file.write(content)
|
||||
file.close()
|
||||
return filename
|
||||
|
||||
def get(name):
|
||||
return send_from_directory(current_app.config["UPLOAD_FOLDER"], name)
|
13
kinolist/schema.sql
Normal file
13
kinolist/schema.sql
Normal file
@ -0,0 +1,13 @@
|
||||
DROP TABLE IF EXISTS movies;
|
||||
|
||||
CREATE TABLE movies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
number INTEGER,
|
||||
title TEXT,
|
||||
year INTEGER,
|
||||
poster TEXT,
|
||||
plot TEXT,
|
||||
watched INTEGER,
|
||||
watchDate TEXT,
|
||||
score NUMBER
|
||||
)
|
BIN
kinolist/static/favicon.jpg
Normal file
BIN
kinolist/static/favicon.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 200 KiB |
192
kinolist/static/styles.css
Normal file
192
kinolist/static/styles.css
Normal file
@ -0,0 +1,192 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: 20px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display: inline;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
article, form {
|
||||
text-align: left;
|
||||
min-width: 200px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0px 0px 10px #ccc;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
text-align: left;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
aside {
|
||||
color: #ccc;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.styled-table {
|
||||
border-collapse: collapse;
|
||||
margin: 25px 0;
|
||||
font-size: 0.9em;
|
||||
font-family: sans-serif;
|
||||
min-width: 400px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.styled-table thead tr {
|
||||
background-color: #0056b3;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.styled-table th,
|
||||
.styled-table td {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
.styled-table tbody tr {
|
||||
border-bottom: 1px solid #dddddd;
|
||||
}
|
||||
|
||||
.styled-table tbody tr:nth-of-type(even) {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
.styled-table tbody tr:last-of-type {
|
||||
border-bottom: 2px solid #0056b3;
|
||||
}
|
||||
|
||||
.styled-table tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.styled-table tbody tr.active-row {
|
||||
font-weight: bold;
|
||||
color: #0056b3;
|
||||
}
|
||||
|
||||
fieldset td:last-child {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.styled-table button {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.styled-table { border-collapse: collapse; }
|
||||
tr { border: none; }
|
||||
.styled-table td {
|
||||
border-left: solid 1px #007bff;
|
||||
}
|
||||
|
||||
.styled-table td:first-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.new-entry {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top:0;
|
||||
overflow: hidden;
|
||||
padding: 5px;
|
||||
background-color: #0056b3;
|
||||
box-shadow: 3px 3px #003e81;
|
||||
}
|
||||
|
||||
.large-input {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.new-entry button {
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.manage td:not(:last-child):not(:first-child):hover {
|
||||
background-color: pink;
|
||||
}
|
||||
|
||||
.manage td:not(:last-child):not(:first-child) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: lightblue !important;
|
||||
}
|
||||
|
||||
.transparent-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: black;
|
||||
opacity: 0.6;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.title-list {
|
||||
background-color: white;
|
||||
position: fixed;
|
||||
top: 10%;
|
||||
bottom: 10%;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
z-index: 1050;
|
||||
}
|
||||
|
||||
.title-list div {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.title-list button {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.choice-list {
|
||||
height: 95%;
|
||||
border-bottom: solid 1px #007bff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow: scroll;
|
||||
}
|
38
kinolist/templates/_choicelist.html
Normal file
38
kinolist/templates/_choicelist.html
Normal file
@ -0,0 +1,38 @@
|
||||
<div class="choice-list">
|
||||
<table class="styled-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Choose</th>
|
||||
<th>Title</th>
|
||||
<th>Year</th>
|
||||
<th>Poster</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for movie in movies %}
|
||||
<tr id={{ movie["imdbID"] }}>
|
||||
<td>
|
||||
<input type="radio" name="choice">
|
||||
</td>
|
||||
<td>
|
||||
{% autoescape false %}
|
||||
{{ movie["Title"]|replace("\n","<br>")}}
|
||||
{% endautoescape %}
|
||||
</td>
|
||||
<td>
|
||||
{{ movie["Year"] }}
|
||||
</td>
|
||||
<td>
|
||||
{% if movie["Poster"] != "N/A" %}
|
||||
<img src="{{ movie["Poster"] }}" width={{ w }}>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="makeChoice()">OK</button>
|
||||
<button onclick="cancelChoice()">Cancel</button>
|
||||
</div>
|
33
kinolist/templates/_line.html
Normal file
33
kinolist/templates/_line.html
Normal file
@ -0,0 +1,33 @@
|
||||
<tr id={{ movie["id"] }}>
|
||||
{%if manage %}
|
||||
<td>
|
||||
<button onclick="changePosition({{ movie["id"] }})">Move</button>
|
||||
</td>{% endif %}
|
||||
<td {%if manage %}onfocusout="changeTitle(this,{{ movie["id"] }})" contenteditable{% endif %}>{% if movie["title"] %}
|
||||
{% autoescape false %}
|
||||
{{ movie["title"]|replace("\n","<br>")}}
|
||||
{% endautoescape %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td {%if manage %}onfocusout="changeYear(this,{{ movie["id"] }})" contenteditable{% endif %}>{% if movie["year"] %}
|
||||
{{ movie["year"] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td {%if manage %}onclick="changePoster(this,{{ movie["id"] }})"{% endif %}>
|
||||
{% if movie["poster"] %}
|
||||
<img src="/posters/{{ movie['poster'] }}" width={{ w }}>
|
||||
{% else %}
|
||||
<img src="" width={{ w }}>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td {%if manage %}onfocusout="changePlot(this,{{ movie["id"] }})" contenteditable{% endif %}>{% if movie["plot"] %}
|
||||
{% autoescape false %}
|
||||
{{ movie["plot"]|replace("\n","<br>")}}
|
||||
{% endautoescape %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{%if manage %}<td {% if not movie["watched"] %}colspan="3"{% endif %} onclick="changeWatched(this,{{ movie["id"] }})">{%if movie["watched"] %}✔{% else %}✘{% endif %}</td>{% endif %}
|
||||
{%if manage or watched %}<td {%if manage %}onfocusout="changeWatchDate(this,{{ movie["id"] }})"{% endif %} {% if not movie["watched"] %}hidden{% endif %}>{%if manage %}<input type="date" id="date{{ movie["id"] }}" name="date{{ movie["id"] }}" value="{% if movie["watchDate"] %}{{ movie["watchDate"] }}{% endif %}">{% else %}{% if movie["watchDate"] %}{{ movie["watchDate"] }}{% endif %}{% endif %}</td>
|
||||
<td {%if manage %}onfocusout="changeScore(this,{{ movie["id"] }})" contenteditable{% endif %} {% if not movie["watched"] %}hidden{% endif %}>{% if movie["score"] %}{{ movie["score"] }}{% endif %}</td>{% endif %}
|
||||
{% if manage %}<td><button onclick="deleteRow({{ movie["id"] }})">Delete</button></td>{% endif %}
|
||||
</tr>
|
20
kinolist/templates/_movies.html
Normal file
20
kinolist/templates/_movies.html
Normal file
@ -0,0 +1,20 @@
|
||||
<table class="styled-table{%if manage %} manage{% endif %}">
|
||||
<thead>
|
||||
<tr>
|
||||
{%if manage %}<th></th>{% endif %}
|
||||
<th>Title</th>
|
||||
<th>Year</th>
|
||||
<th>Poster</th>
|
||||
<th>Summary</th>
|
||||
{%if manage %}<th>Already watched?</th>{% endif %}
|
||||
{%if manage or watched %}<th>Watch date</th>
|
||||
<th>Score</th>{% endif %}
|
||||
{%if manage %}<th></th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody">
|
||||
{% for movie in movies %}
|
||||
{% include "_line.html" %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
7
kinolist/templates/_navigation.html
Normal file
7
kinolist/templates/_navigation.html
Normal file
@ -0,0 +1,7 @@
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('pages.home') }}">Home</a></li>
|
||||
<li><a href="{{ url_for('pages.manage') }}">Manage</a></li>
|
||||
{% block navigation %}{% endblock navigation %}
|
||||
</ul>
|
||||
</nav>
|
31
kinolist/templates/base.html
Normal file
31
kinolist/templates/base.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Movies List - {% block title %} {% endblock title %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.jpg') }}">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Movies List</h1>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('pages.to_watch') }}">Movie List</a></li>
|
||||
<li><a href="{{ url_for('pages.watched') }}">Watched Movies</a></li>
|
||||
<li><a href="{{ url_for('pages.manage') }}">Manage</a></li>
|
||||
{% block navigation %}{% endblock navigation %}
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
<header>
|
||||
{% block header %}{% endblock header %}
|
||||
</header>
|
||||
<main>
|
||||
{% block content %}{% endblock content %}
|
||||
</main>
|
||||
</section>
|
||||
<script>
|
||||
{% block script %}{%endblock script %}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
9
kinolist/templates/pages/home.html
Normal file
9
kinolist/templates/pages/home.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block header %}
|
||||
<h2>{% block title%}Home{% endblock title %}</h2>
|
||||
{% endblock header %}
|
||||
|
||||
{% block content %}
|
||||
{% include('_movies.html') %}
|
||||
{% endblock content %}
|
16
kinolist/templates/pages/login.html
Normal file
16
kinolist/templates/pages/login.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block header %}
|
||||
<h2>{% block title%}Login{% endblock title %}</h2>
|
||||
{% endblock header %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<fieldset>
|
||||
<legend>Login</legend>
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" name="password" id="password" required>
|
||||
<input type="submit" value="Login">
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endblock content %}
|
31
kinolist/templates/pages/manage.html
Normal file
31
kinolist/templates/pages/manage.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block header %}
|
||||
<h2>{% block title%}Home{% endblock title %}</h2>
|
||||
{% endblock header %}
|
||||
|
||||
{% block navigation %}
|
||||
<li><a href="{{ url_for('pages.logout') }}">Logout</a></li>
|
||||
{% endblock navigation %}
|
||||
|
||||
{% block content %}
|
||||
<div class="new-entry">
|
||||
<fieldset>
|
||||
<legend>Select a type of new entry:</legend>
|
||||
<select name="choice" id="choice">
|
||||
<option value="title">Title</option>
|
||||
<option value="imdbid">ImdbID</option>
|
||||
<option value="blank">Blank entry</option>
|
||||
</select>
|
||||
<input class="large-input" id="input-text" type="text">
|
||||
</fieldset>
|
||||
<button id="newEntry" onclick="newEntry()">New entry</button>
|
||||
</div>
|
||||
|
||||
<input id="upload" type="file" accept="image/png, image/jpeg" onchange="newFile(this)" hidden>
|
||||
{% include('_movies.html') %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block script %}
|
||||
{% include('pages/manage.js') %}
|
||||
{% endblock script %}
|
239
kinolist/templates/pages/manage.js
Normal file
239
kinolist/templates/pages/manage.js
Normal file
@ -0,0 +1,239 @@
|
||||
const upload = document.getElementById("upload");
|
||||
const tbody = document.getElementById("tbody");
|
||||
const main = document.getElementsByTagName("main")[0];
|
||||
const choice = document.getElementById("choice");
|
||||
const textInput = document.getElementById("input-text");
|
||||
|
||||
const choiceList = document.createElement("div");
|
||||
choiceList.classList.add("title-list");
|
||||
const transpBackground = document.createElement("div");
|
||||
transpBackground.classList.add("transparent-background");
|
||||
|
||||
const extraLine = document.createElement("tr");
|
||||
extraLine.id = "end";
|
||||
extraLine.innerHTML = `<td colspan=7><button onclick=changePosition("end")>Here</td>`;
|
||||
|
||||
function getText(elem) { // I don't know what I am doing with this function, it may not be portable
|
||||
if (elem.nodeType == Node.TEXT_NODE) {
|
||||
return elem.data;
|
||||
} else if (elem.nodeName === "BR") {
|
||||
return "";
|
||||
} else {
|
||||
let res = "";
|
||||
const l = elem.childNodes;
|
||||
for (const i of l) {
|
||||
res = res + getText(i);
|
||||
}
|
||||
if (elem.nodeName === "DIV") res = res + "\n";
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let res = undefined; // sets the receiver of the file
|
||||
let rej = undefined;
|
||||
|
||||
function newFile(elem) { // send the file to whoever wants the file
|
||||
if (elem.files.length = 1) {
|
||||
res(elem.files[0]);
|
||||
}
|
||||
else rej();
|
||||
}
|
||||
|
||||
function getFile() { // open the upload form, and ask to send the file asynchronously
|
||||
upload.click();
|
||||
return new Promise((resolve, reject) => {
|
||||
res = resolve;
|
||||
rej = reject;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function newEntry() {
|
||||
const form = new FormData();
|
||||
const value = choice.value;
|
||||
|
||||
form.append("new", true);
|
||||
if (value === "title") {
|
||||
form.append("choice", "title");
|
||||
form.append("title", textInput.value)
|
||||
} else if (value === "imdbid") {
|
||||
form.append("choice", "imdbid");
|
||||
form.append("imdbid", textInput.value);
|
||||
} else if (value === "blank") {
|
||||
form.append("choice", "blank");
|
||||
} else { return; }
|
||||
const html = await (await fetch("/manage", {method: "POST",
|
||||
body: form})).text();
|
||||
if (value === "title") {
|
||||
if (html === "") {
|
||||
alert("Problem requesting title")
|
||||
} else {
|
||||
choiceList.innerHTML = html;
|
||||
main.appendChild(choiceList);
|
||||
main.appendChild(transpBackground);
|
||||
}
|
||||
} else if (html === "") {
|
||||
alert("Movie not found");
|
||||
} else tbody.innerHTML = tbody.innerHTML + html;
|
||||
}
|
||||
|
||||
async function makeChoice() {
|
||||
const radios = choiceList.getElementsByTagName("input");
|
||||
for (radio of radios) {
|
||||
if (radio.checked) {
|
||||
const form = new FormData();
|
||||
form.append("new", true);
|
||||
form.append("choice","imdbid");
|
||||
form.append("imdbid", radio.parentNode.parentNode.id);
|
||||
const html = await (await fetch("/manage", {method: "POST",
|
||||
body: form})).text();
|
||||
tbody.innerHTML = tbody.innerHTML + html;
|
||||
cancelChoice();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cancelChoice() {
|
||||
main.removeChild(choiceList);
|
||||
main.removeChild(transpBackground);
|
||||
}
|
||||
|
||||
{
|
||||
let moveStarted = false;
|
||||
let source;
|
||||
|
||||
function changePosition(id) {
|
||||
const list = tbody.getElementsByTagName("tr");
|
||||
if (moveStarted) {
|
||||
moveStarted = false;
|
||||
const tr1 = document.getElementById(source);
|
||||
const tr2 = document.getElementById(id);
|
||||
tbody.insertBefore(tr1, tr2);
|
||||
tbody.removeChild(extraLine);
|
||||
for (const tr of list) {
|
||||
tr.children[0].children[0].innerText="Move";
|
||||
if (tr.id == source) {
|
||||
tr.classList.remove("selected");
|
||||
}
|
||||
}
|
||||
const form = new FormData();
|
||||
form.append("id", id);
|
||||
form.append("move", source);
|
||||
fetch("/manage", {method: "POST",
|
||||
body: form});
|
||||
} else {
|
||||
moveStarted = true;
|
||||
source=id;
|
||||
for (const tr of list) {
|
||||
tr.children[0].children[0].innerText="Here";
|
||||
if (tr.id == id) {
|
||||
tr.classList.add("selected");
|
||||
}
|
||||
}
|
||||
tbody.appendChild(extraLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeTitle(elem, id) {
|
||||
const title = getText(elem).trim();
|
||||
const form = new FormData();
|
||||
form.append("id", id);
|
||||
form.append("title", title);
|
||||
fetch("/manage", {method: "POST",
|
||||
body: form});
|
||||
}
|
||||
|
||||
function to_int(elem) {
|
||||
let value = "";
|
||||
const oldstring = elem.innerHTML;
|
||||
for (let c of oldstring) {
|
||||
if (c >= "0" && c <= 9) value = value + c;
|
||||
}
|
||||
while (value.length > 1 && value[0] === "0") { value = value.substring(1); }
|
||||
elem.innerHTML = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
function changeYear(elem, id) {
|
||||
const year = to_int(elem);
|
||||
const form = new FormData();
|
||||
form.append("id", id);
|
||||
form.append("year", year);
|
||||
fetch("/manage", {method: "POST",
|
||||
body: form});
|
||||
}
|
||||
|
||||
async function changePoster(elem, id) {
|
||||
poster = await getFile();
|
||||
const form = new FormData();
|
||||
form.append("id", id);
|
||||
form.append("poster", poster);
|
||||
file = await(await (fetch("/manage", {method: "POST",
|
||||
body: form}))).text();
|
||||
elem.getElementsByTagName("img")[0].src = "/posters/" + file;
|
||||
}
|
||||
|
||||
async function changePlot(elem, id) {
|
||||
const plot = getText(elem).trim();
|
||||
const form = new FormData();
|
||||
form.append("id", id);
|
||||
form.append("plot", plot);
|
||||
fetch("/manage", {method: "POST",
|
||||
body: form});
|
||||
}
|
||||
|
||||
function changeWatched(elem, id) {
|
||||
let watched;
|
||||
if (elem.innerText.trim() === "✘") {
|
||||
elem.innerHTML = "✔";
|
||||
elem.removeAttribute("colspan");
|
||||
e = elem.nextElementSibling;
|
||||
e.removeAttribute("hidden");
|
||||
e.nextElementSibling.removeAttribute("hidden");
|
||||
watched = 1;
|
||||
} else {
|
||||
elem.innerHTML = "✘";
|
||||
elem.setAttribute("colspan", 3);
|
||||
e = elem.nextElementSibling;
|
||||
e.setAttribute("hidden", "");
|
||||
e.nextElementSibling.setAttribute("hidden", "");
|
||||
watched = 0;
|
||||
}
|
||||
const form = new FormData();
|
||||
form.append("id", id);
|
||||
form.append("watched", watched);
|
||||
fetch("/manage", {method: "POST",
|
||||
body: form});
|
||||
}
|
||||
|
||||
function changeWatchDate(elem, id) {
|
||||
const form = new FormData();
|
||||
form.append("id", id);
|
||||
form.append("watchDate", elem.getElementsByTagName("input")[0].value);
|
||||
fetch("/manage", {method: "POST",
|
||||
body: form});
|
||||
}
|
||||
|
||||
function changeScore(elem, id) {
|
||||
const score = to_int(elem);
|
||||
const form = new FormData();
|
||||
form.append("id", id);
|
||||
form.append("score", score);
|
||||
fetch("/manage", {method: "POST",
|
||||
body: form});
|
||||
}
|
||||
|
||||
function deleteRow(id) {
|
||||
if (confirm("Are you sure you want to delete this entry?")) {
|
||||
const line = document.getElementById(id);
|
||||
line.parentNode.removeChild(line);
|
||||
const form = new FormData();
|
||||
form.append("id", id);
|
||||
form.append("delete", true);
|
||||
fetch("/manage", {method: "POST",
|
||||
body: form});
|
||||
}
|
||||
}
|
9
kinolist/templates/pages/to_watch.html
Normal file
9
kinolist/templates/pages/to_watch.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block header %}
|
||||
<h2>{% block title%}Movies To Watch{% endblock title %}</h2>
|
||||
{% endblock header %}
|
||||
|
||||
{% block content %}
|
||||
{% include('_movies.html') %}
|
||||
{% endblock content %}
|
9
kinolist/templates/pages/watched.html
Normal file
9
kinolist/templates/pages/watched.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block header %}
|
||||
<h2>{% block title%}Watched Movies{% endblock title %}</h2>
|
||||
{% endblock header %}
|
||||
|
||||
{% block content %}
|
||||
{% include('_movies.html') %}
|
||||
{% endblock content %}
|
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -0,0 +1,6 @@
|
||||
click
|
||||
Flask
|
||||
Flask-Login
|
||||
python-dotenv
|
||||
requests
|
||||
|
Loading…
Reference in New Issue
Block a user