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