first commit (create a local server)

This commit is contained in:
bartholin 2024-10-08 19:20:00 +02:00
commit 2a4201d68f
26 changed files with 1033 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
env
.env
movielist.sqlite
posters/*
__pycache__
*~

55
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
../posters

15
kinolist/posters.py Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

192
kinolist/static/styles.css Normal file
View 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;
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 %}

View 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 %}

View 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 %}

View 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});
}
}

View 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 %}

View 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
View File

@ -0,0 +1,6 @@
click
Flask
Flask-Login
python-dotenv
requests