Files
photoassistant/files.py

736 lines
35 KiB
Python

from wtforms import SubmitField, StringField, HiddenField, validators, Form
from flask_wtf import FlaskForm
from flask import request, render_template, redirect, send_from_directory, url_for, jsonify
from path import MovePathDetails
from main import db, app, ma
from sqlalchemy import Sequence
from sqlalchemy.exc import SQLAlchemyError
from status import st, Status
import os
import glob
from PIL import Image
from pymediainfo import MediaInfo
import hashlib
import exifread
import base64
import numpy
import cv2
import time
import re
import json
import datetime
from flask_login import login_required, current_user
from states import States
################################################################################
# Local Class imports
################################################################################
from job import Job, JobExtra, Joblog, NewJob
from path import PathType, Path
from person import Refimg, Person, PersonRefimgLink
from settings import Settings, SettingsIPath, SettingsSPath, SettingsRBPath
from shared import SymlinkName
from dups import Duplicates
from face import Face, FaceFileLink, FaceRefimgLink
# pylint: disable=no-member
################################################################################
# Class describing PathDirLink and in the DB (via sqlalchemy)
# connects the entry (dir) with a path
################################################################################
class PathDirLink(db.Model):
__tablename__ = "path_dir_link"
path_id = db.Column(db.Integer, db.ForeignKey("path.id"), primary_key=True )
dir_eid = db.Column(db.Integer, db.ForeignKey("dir.eid"), primary_key=True )
def __repr__(self):
return f"<path_id: {self.path_id}, dir_eid: {self.dir_eid}>"
################################################################################
# Class describing EntryDirLInk and in the DB (via sqlalchemy)
# connects (many) entry contained in a directory (which is also an entry)
################################################################################
class EntryDirLink(db.Model):
__tablename__ = "entry_dir_link"
entry_id = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
dir_eid = db.Column(db.Integer, db.ForeignKey("dir.eid"), primary_key=True )
def __repr__(self):
return f"<entry_id: {self.entry_id}, dir_eid: {self.dir_eid}>"
################################################################################
# Class describing Dir and in the DB (via sqlalchemy)
# rel_path: rest of dir after path, e.g. if path = /..../storage, then
# rel_path could be 2021/20210101-new-years-day-pics
# in_path: only in this structure, not DB, quick ref to the path this dir is in
################################################################################
class Dir(db.Model):
__tablename__ = "dir"
eid = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
rel_path = db.Column(db.String, unique=True )
in_path = db.relationship("Path", secondary="path_dir_link", uselist=False)
def __repr__(self):
return f"<eid: {self.eid}, rel_path: {self.rel_path}, in_path: {self.in_path}>"
################################################################################
# Class describing Entry and in the DB (via sqlalchemy)
# an entry is the common bits between files and dirs
# type is a convenience var only in this class, not in DB
# {dir|file}_etails are convenience data for the relevant details from the Dir
# or File class - not in DB
# in_dir - is the Dir that this entry is located in (convenience for class only)
# FullPathOnFS(): method to get path on the FS for this Entry
################################################################################
class Entry(db.Model):
__tablename__ = "entry"
id = db.Column(db.Integer, db.Sequence('file_id_seq'), primary_key=True )
name = db.Column(db.String, unique=False, nullable=False )
type_id = db.Column(db.Integer, db.ForeignKey("file_type.id"))
type = db.relationship("FileType")
dir_details = db.relationship( "Dir", uselist=False )
file_details = db.relationship( "File", uselist=False )
in_dir = db.relationship ("Dir", secondary="entry_dir_link", uselist=False )
def FullPathOnFS(self):
if self.in_dir:
s=self.in_dir.in_path.path_prefix + '/'
if len(self.in_dir.rel_path) > 0:
s += self.in_dir.rel_path + '/'
s += self.name
# this occurs when we have a dir that is the root of a path
else:
s=self.dir_details.in_path.path_prefix
return s
def __repr__(self):
return f"<id: {self.id}, name: {self.name}, type={self.type}, dir_details={self.dir_details}, file_details={self.file_details}, in_dir={self.in_dir}"
################################################################################
# Class describing File and in the DB (via sqlalchemy)
# all files are entries, this is the extra bits only for a file, of note:
# hash is unique for files, and used to validate duplicates
# woy == week of year, all date fields are used to sort/show content. Date
# info can be from exif, or file system, or file name (rarely)
# faces: convenience field to show connected face(s) for this file
################################################################################
class File(db.Model):
__tablename__ = "file"
eid = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
size_mb = db.Column(db.Integer, unique=False, nullable=False)
thumbnail = db.Column(db.String, unique=False, nullable=True)
hash = db.Column(db.Integer)
year = db.Column(db.Integer)
month = db.Column(db.Integer)
day = db.Column(db.Integer)
woy = db.Column(db.Integer)
faces = db.relationship ("Face", secondary="face_file_link" )
def __repr__(self):
return f"<eid: {self.eid}, size_mb={self.size_mb}, hash={self.hash}, year={self.year}, month={self.month}, day={self.day}, woy={self.woy}, faces={self.faces}>"
################################################################################
# Class describing FileType and in the DB (via sqlalchemy)
# pre-defined list of file types (image, dir, etc.)
################################################################################
class FileType(db.Model):
__tablename__ = "file_type"
id = db.Column(db.Integer, db.Sequence('file_type_id_seq'), primary_key=True )
name = db.Column(db.String, unique=True, nullable=False )
def __repr__(self):
return f"<id: {self.id}, name={self.name}>"
################################################################################
# Class describing PA_JobManager_Message and in the DB (via sqlalchemy)
# the job manager can send a message back to the front end (this code) via the
# DB. has to be about a specific job_id and is success/danger, etc. (alert)
# and a message
################################################################################
class PA_JobManager_Message(db.Model):
__tablename__ = "pa_job_manager_fe_message"
id = db.Column(db.Integer, db.Sequence('pa_job_manager_fe_message_id_seq'), primary_key=True )
job_id = db.Column(db.Integer, db.ForeignKey('job.id') )
alert = db.Column(db.String)
message = db.Column(db.String)
job = db.relationship ("Job" )
def __repr__(self):
return f"<id: {self.id}, job_id: {self.job_id}, alert: {self.alert}, message: {self.message}, job: {self.job}"
################################################################################
# GetJM_Message: used in html to display any message for this front-end
################################################################################
def GetJM_Message():
msg=PA_JobManager_Message.query.first()
return msg
################################################################################
# ClearJM_Message: used in html to clear any message just displayed
################################################################################
def ClearJM_Message(id):
PA_JobManager_Message.query.filter(PA_JobManager_Message.id==id).delete()
db.session.commit()
return
################################################################################
# GetEntriesInFlatView: func. to retrieve DB entries appropriate for flat view
################################################################################
def GetEntriesInFlatView( OPT, prefix ):
entries=[]
if OPT.noo == "Oldest":
entries+=Entry.query.join(File).join(EntryDirLink).join(Dir).join(PathDirLink).join(Path).filter(Path.path_prefix==prefix).order_by(File.year,File.month,File.day,Entry.name).offset(OPT.offset).limit(OPT.how_many).all()
elif OPT.noo == "Newest":
entries+=Entry.query.join(File).join(EntryDirLink).join(Dir).join(PathDirLink).join(Path).filter(Path.path_prefix==prefix).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(OPT.offset).limit(OPT.how_many).all()
elif OPT.noo == "Z to A":
entries+=Entry.query.join(File).join(EntryDirLink).join(Dir).join(PathDirLink).join(Path).filter(Path.path_prefix==prefix).order_by(Entry.name.desc()).offset(OPT.offset).limit(OPT.how_many).all()
else:
entries+=Entry.query.join(File).join(EntryDirLink).join(Dir).join(PathDirLink).join(Path).filter(Path.path_prefix==prefix).order_by(Entry.name).offset(OPT.offset).limit(OPT.how_many).all()
return entries
################################################################################
# GetEntriesInFolderView: func. to retrieve DB entries appropriate for folder view
# read inline comments to deal with variations / ordering...
################################################################################
def GetEntriesInFolderView( OPT, prefix ):
entries=[]
# okay the root cwd is fake, so treat it specially - its Dir can be found by path with dir.rel_path=''
if os.path.dirname(OPT.cwd) == 'static':
dir=Entry.query.join(Dir).join(PathDirLink).join(Path).filter(Dir.rel_path=='').filter(Path.path_prefix==prefix).order_by(Entry.name).first()
# this can occur if the path in settings does not exist as it wont be in # the DB
if not dir:
return entries
# although this is 1 entry, needs to come back via all() to be iterable
entries+= Entry.query.filter(Entry.id==dir.id).all()
else:
rp = OPT.cwd.replace( prefix, '' )
# when in subdirs, replacing prefix will leave the first char as /, get rid of it
if len(rp) and rp[0] == '/':
rp=rp[1:]
dir=Entry.query.join(Dir).join(PathDirLink).join(Path).filter(Dir.rel_path==rp).filter(Path.path_prefix==prefix).order_by(Entry.name).first()
# this can occur if the path in settings does not exist as it wont be in # the DB
if not dir:
return entries
if OPT.noo == "Z to A" or "Newest":
entries+= Entry.query.join(EntryDirLink).join(FileType).filter(EntryDirLink.dir_eid==dir.id).filter(FileType.name=='Directory').order_by(Entry.name.desc()).all()
# just do A to Z / Oldest by default or if no valid option
else:
entries+= Entry.query.join(EntryDirLink).join(FileType).filter(EntryDirLink.dir_eid==dir.id).filter(FileType.name=='Directory').order_by(Entry.name).all()
# add any files at the current CWD (based on dir_eid in DB)
if OPT.noo == "Oldest":
entries+=Entry.query.join(File).join(EntryDirLink).filter(EntryDirLink.dir_eid==dir.id).order_by(File.year,File.month,File.day,Entry.name).offset(OPT.offset).limit(OPT.how_many).all()
elif OPT.noo == "Newest":
entries+=Entry.query.join(File).join(EntryDirLink).filter(EntryDirLink.dir_eid==dir.id).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(OPT.offset).limit(OPT.how_many).all()
elif OPT.noo == "Z to A":
entries+=Entry.query.join(File).join(EntryDirLink).filter(EntryDirLink.dir_eid==dir.id).order_by(Entry.name.desc()).offset(OPT.offset).limit(OPT.how_many).all()
# just do A to Z by default or if no valid option
else:
entries+=Entry.query.join(File).join(EntryDirLink).filter(EntryDirLink.dir_eid==dir.id).order_by(Entry.name).offset(OPT.offset).limit(OPT.how_many).all()
return entries
################################################################################
# /GetEntries -> helper function that Gets Entries for required files to show
# for several routes (files_ip, files_sp, files_rbp, search, viewlist)
################################################################################
def GetEntries( OPT ):
entries=[]
if OPT.path_type == 'Search' or (OPT.path_type == 'View' and OPT.orig_ptype=='Search'):
print( f"getting entries: OPT={OPT}" )
search_term=OPT.orig_search_term
if 'AI:' in search_term:
search_term = search_term.replace('AI:','')
all_entries = Entry.query.join(File).join(FaceFileLink).join(Face).join(FaceRefimgLink).join(Refimg).join(PersonRefimgLink).join(Person).filter(Person.tag.ilike(f"%{search_term}%")).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(OPT.offset).limit(OPT.how_many).all()
else:
file_data=Entry.query.join(File).filter(Entry.name.ilike(f"%{search_term}%")).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(OPT.offset).limit(OPT.how_many).all()
dir_data=Entry.query.join(File).join(EntryDirLink).join(Dir).filter(Dir.rel_path.ilike(f"%{search_term}%")).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(OPT.offset).limit(OPT.how_many).all()
ai_data=Entry.query.join(File).join(FaceFileLink).join(Face).join(FaceRefimgLink).join(Refimg).join(PersonRefimgLink).join(Person).filter(Person.tag.ilike(f"%{search_term}%")).order_by(File.year.desc(),File.month.desc(),File.day.desc(),Entry.name).offset(OPT.offset).limit(OPT.how_many).all()
# remove any duplicates from combined data
all_entries = []
for f in file_data:
all_entries.append(f)
for d in dir_data:
add_it=1
for f in file_data:
if d.name == f.name:
add_it=0
break
if add_it:
all_entries.append(d)
for a in ai_data:
add_it=1
for f in file_data:
if a.name == f.name:
add_it=0
break
if add_it:
all_entries.append(a)
return all_entries
# if we are a view, then it will be of something else, e.g. a list of
# import, storage, or bin images, reset OPT.path_type so that the paths array below works
if 'View' in OPT.path_type:
eid = OPT.url[6:]
print( f"we have a view, eid={eid}" )
#e=Entry.query.get(eid)
#OPT.path_type=e.in_dir.in_path.type.name
print( f"pt={OPT.orig_ptype}, st={OPT.orig_search_term}" )
OPT.path_type= OPT.orig_ptype
paths = []
if OPT.path_type == 'Storage':
paths = SettingsSPath()
elif OPT.path_type == 'Import':
paths = SettingsIPath()
elif OPT.path_type == 'Bin':
paths.append(SettingsRBPath())
for path in paths:
if not os.path.exists(path):
continue
prefix = SymlinkName(OPT.path_type,path,path+'/')
if OPT.folders:
entries+=GetEntriesInFolderView( OPT, prefix )
else:
entries+=GetEntriesInFlatView( OPT, prefix )
return entries
################################################################################
# /clear_jm_msg -> with a dismissable error (ie. anything not success, that is
# not showing duplicates (so rare errors) - allow them to be dismissed
################################################################################
@app.route("/clear_jm_msg/<id>", methods=["POST"])
@login_required
def clear_jm_msg(id):
ClearJM_Message(id)
return redirect( url_for("main_page") )
@app.route("/ChangeFileOpts", methods=["POST"])
@login_required
def ChangeFileOpts():
# reset options based on form post, then redirect back to orig page
OPT=States( request )
for el in request.form:
print( f"{el}={request.form[el]}")
return redirect( request.referrer )
################################################################################
# /file_list -> show detailed file list of files from import_path(s)
################################################################################
@app.route("/file_list_ip", methods=["GET", "POST"])
@login_required
def file_list_ip():
OPT=States( request )
# now we have reset the offset, etc. into the prefs, we can use a GET and this will be back/forward browser button safe
if request.method=='POST':
redirect("/file_list_ip")
entries=GetEntries( OPT )
return render_template("file_list.html", page_title='View File Details (Import Path)', entry_data=entries, OPT=OPT )
################################################################################
# /files -> show thumbnail view of files from import_path(s)
################################################################################
@app.route("/files_ip", methods=["GET", "POST"])
@login_required
def files_ip():
OPT=States( request )
# now we have reset the offset, etc. into the prefs, we can use a GET and this will be back/forward browser button safe
if request.method=='POST':
redirect("/files_ip")
entries=GetEntries( OPT )
people = Person.query.all()
move_paths = MovePathDetails()
return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", entry_data=entries, OPT=OPT, people=people, move_paths=move_paths )
################################################################################
# /files -> show thumbnail view of files from storage_path
################################################################################
@app.route("/files_sp", methods=["GET", "POST"])
@login_required
def files_sp():
OPT=States( request )
# now we have reset the offset, etc. into the prefs, we can use a GET and this will be back/forward browser button safe
if request.method=='POST':
redirect("/files_sp")
entries=GetEntries( OPT )
people = Person.query.all()
move_paths = MovePathDetails()
return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", entry_data=entries, OPT=OPT, people=people, move_paths=move_paths )
################################################################################
# /files -> show thumbnail view of files from recycle_bin_path
################################################################################
@app.route("/files_rbp", methods=["GET", "POST"])
@login_required
def files_rbp():
OPT=States( request )
# now we have reset the offset, etc. into the prefs, we can use a GET and this will be back/forward browser button safe
if request.method=='POST':
redirect("/files_rbp")
entries=GetEntries( OPT )
people = Person.query.all()
move_paths = MovePathDetails()
return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", entry_data=entries, OPT=OPT, move_paths=move_paths )
################################################################################
# search -> GET version -> has search_term in the URL and is therefore able to
# be used even if the user hits the front/back buttons in the browser.
# func shows thumbnails of matching files.
################################################################################
@app.route("/search/<search_term>", methods=["GET", "POST"])
@login_required
def search(search_term):
OPT=States( request )
# if we posted to get here, its a change in State, so save it to pa_user_state, and go back to the GET version or URL
if request.method=="POST":
redirect("/search/"+search_term)
OPT.search_term = search_term
# always show flat results for search to start with
OPT.folders=False
entries=GetEntries( OPT )
move_paths = MovePathDetails()
return render_template("files.html", page_title='View Files', search_term=search_term, entry_data=entries, OPT=OPT, move_paths=move_paths )
################################################################################
# /search -> POST version -> only used on form submit when you hit return. This
# form just redirects to a GET of /search/<search_term> to trip route above
################################################################################
@app.route("/search", methods=["POST"])
@login_required
def search_post():
return redirect( "/search/"+request.form['search_term'] )
################################################################################
# /files/scannow -> allows us to force a check for new files
################################################################################
@app.route("/files/scannow", methods=["GET"])
@login_required
def scannow():
job=NewJob("scannow" )
st.SetMessage("scanning for new files in:&nbsp;<a href=/job/{}>Job #{}</a>&nbsp;(Click the link to follow progress)".format( job.id, job.id) )
return render_template("base.html")
################################################################################
# /files/forcescan -> deletes old data in DB, and does a brand new scan
################################################################################
@app.route("/files/forcescan", methods=["GET"])
@login_required
def forcescan():
job=NewJob("forcescan" )
st.SetMessage("force scan & rebuild data for files in:&nbsp;<a href=/job/{}>Job #{}</a>&nbsp;(Click the link to follow progress)".format( job.id, job.id) )
return render_template("base.html")
################################################################################
# /files/scan_sp -> allows us to force a check for new files
################################################################################
@app.route("/files/scan_sp", methods=["GET"])
@login_required
def scan_sp():
job=NewJob("scan_sp" )
st.SetMessage("scanning for new files in:&nbsp;<a href=/job/{}>Job #{}</a>&nbsp;(Click the link to follow progress)".format( job.id, job.id) )
return render_template("base.html")
################################################################################
# /fix_dups -> use sql to find duplicates based on same hash, different
# filenames, or directories. Pass this straight through to the job manager
# as job extras to a new job.
################################################################################
@app.route("/fix_dups", methods=["POST"])
@login_required
def fix_dups():
rows = db.engine.execute( "select e1.id as id1, f1.hash, d1.rel_path as rel_path1, d1.eid as did1, e1.name as fname1, p1.id as path1, p1.type_id as path_type1, e2.id as id2, d2.rel_path as rel_path2, d2.eid as did2, e2.name as fname2, p2.id as path2, p2.type_id as path_type2 from entry e1, file f1, dir d1, entry_dir_link edl1, path_dir_link pdl1, path p1, entry e2, file f2, dir d2, entry_dir_link edl2, path_dir_link pdl2, path p2 where e1.id = f1.eid and e2.id = f2.eid and d1.eid = edl1.dir_eid and edl1.entry_id = e1.id and edl2.dir_eid = d2.eid and edl2.entry_id = e2.id and p1.type_id != (select id from path_type where name = 'Bin') and p1.id = pdl1.path_id and pdl1.dir_eid = d1.eid and p2.type_id != (select id from path_type where name = 'Bin') and p2.id = pdl2.path_id and pdl2.dir_eid = d2.eid and f1.hash = f2.hash and e1.id != e2.id and f1.size_mb = f2.size_mb order by path1, rel_path1, fname1");
if rows.returns_rows == False:
st.SetMessage(f"Err, no dups - should now clear the FE 'danger' message?")
return redirect("/")
if 'pagesize' not in request.form:
# default to 10, see if we have a larger value as someone reset it in the gui, rather than first time invoked
pagesize = 10
jexes = JobExtra.query.join(Job).filter(Job.name=='checkdups').filter(Job.pa_job_state=='New').all()
jexes.append( JobExtra( name="pagesize", value=pagesize ) )
else:
pagesize=int(request.form['pagesize'])
DD=Duplicates()
for row in rows:
DD.AddDup( row )
DD.SecondPass()
# DD.Dump()
return render_template("dups.html", DD=DD, pagesize=pagesize )
################################################################################
# /rm_dups -> f/e that shows actual duplicates so that we can delete some dups
# this code creates a new job with extras that have hashes/ids to allow removal
################################################################################
@app.route("/rm_dups", methods=["POST"])
@login_required
def rm_dups():
jex=[]
for el in request.form:
if 'kfhash-' in el:
# get which row/number kf it is...
_, which = el.split('-')
jex.append( JobExtra( name=f"kfid-{which}", value=request.form['kfid-'+which] ) )
jex.append( JobExtra( name=f"kfhash-{which}", value=request.form[el] ) )
if 'kdhash-' in el:
# get which row/number kd it is...
_, which = el.split('-')
jex.append( JobExtra( name=f"kdid-{which}", value=request.form['kdid-'+which] ) )
jex.append( JobExtra( name=f"kdhash-{which}", value=request.form[el] ) )
jex.append( JobExtra( name="pagesize", value=10 ) )
job=NewJob( "rmdups", 0, None, jex )
st.SetMessage( f"Created&nbsp;<a href=/job/{job.id}>Job #{job.id}</a>&nbsp;to delete duplicate files")
return redirect("/jobs")
################################################################################
# /restore_files -> create a job to restore files for the b/e to process
################################################################################
@app.route("/restore_files", methods=["POST"])
@login_required
def restore_files():
jex=[]
for el in request.form:
jex.append( JobExtra( name=f"{el}", value=request.form[el] ) )
job=NewJob( "restore_files", 0, None, jex )
st.SetMessage( f"Created&nbsp;<a href=/job/{job.id}>Job #{job.id}</a>&nbsp;to restore selected file(s)")
return redirect("/jobs")
################################################################################
# /delete_files -> create a job to delete files for the b/e to process
################################################################################
@app.route("/delete_files", methods=["POST"])
@login_required
def delete_files():
jex=[]
for el in request.form:
jex.append( JobExtra( name=f"{el}", value=request.form[el] ) )
job=NewJob( "delete_files", 0, None, jex )
st.SetMessage( f"Created&nbsp;<a href=/job/{job.id}>Job #{job.id}</a>&nbsp;to delete selected file(s)")
return redirect("/jobs")
################################################################################
# /move_files -> create a job to move files for the b/e to process
################################################################################
@app.route("/move_files", methods=["POST"])
@login_required
def move_files():
jex=[]
for el in request.form:
jex.append( JobExtra( name=f"{el}", value=request.form[el] ) )
job=NewJob( "move_files", 0, None, jex )
st.SetMessage( f"Created&nbsp;<a href=/job/{job.id}>Job #{job.id}</a>&nbsp;to move selected file(s)")
return redirect("/jobs")
################################################################################
# /viewlist -> get new set of eids and set current to new img to view
################################################################################
@app.route("/viewlist", methods=["POST"])
@login_required
def viewlist():
OPT=States( request )
# Get next/prev set of data - e.g. if next set, then it will use orig_url
# to go forward how_many from offset and then use viewer.html to show that
# first obj of the new list of entries
entries=GetEntries( OPT )
# this occurs when we went from the last image on a page (with how_many on
# it) and it just happened to also be the last in the DB...
if not entries:
# undo the skip by how_many and getentries again
OPT.offset -= int(OPT.how_many)
entries=GetEntries( OPT )
# now flag we are at the last in db, to reset current below
OPT.last_entry_in_db=1
objs = {}
eids=""
for e in entries:
if not e.file_details:
print( f"seems {e.name} is not a file? -- {e.type}" )
continue
objs[e.id]=e
# get new eids for viewer.html
eids=eids+f"{e.id},"
# put locn data back into array format
for face in e.file_details.faces:
face.locn = json.loads(face.locn)
eids=eids.rstrip(",")
lst = eids.split(',')
if 'next' in request.form:
current = int(lst[0])
if 'prev' in request.form:
current = int(lst[-1])
if hasattr( OPT, 'last_entry_in_db' ):
# force this back to the last image of the last page - its the last in the DB, so set OPT for it
current = int(lst[-1])
OPT.last_entry_in_db=current
return render_template("viewer.html", current=current, eids=eids, objs=objs, OPT=OPT )
@login_required
@app.route("/view/<id>", methods=["GET"])
def view(id):
OPT=States( request )
objs = {}
entries=GetEntries( OPT )
eids=""
for e in entries:
print( f"id={e.id}, len(faces)={len(e.file_details.faces)}")
print(e.id)
objs[e.id]=e
eids += f"{e.id},"
# if this is a dir, we wont view it with a click anyway, so move on...
if not e.file_details:
continue
# put locn data back into array format
for face in e.file_details.faces:
print( f"face.locn before json: {face.locn}" )
face.locn = json.loads(face.locn)
eids=eids.rstrip(",")
return render_template("viewer.html", current=int(id), eids=eids, objs=objs, OPT=OPT )
################################################################################
# /view/id -> grabs data from DB and views it
################################################################################
@app.route("/view/<id>", methods=["POST"])
@login_required
def view_img_post(id):
# set pa_user_states...
OPT=States( request )
# then use back-button friendly URL (and use pa_user_states to view the right image in the right list
return redirect( "/view/" + id );
# route called from front/end - if multiple images are being transformed, each transorm == a separate call
# to this route (and therefore a separate transorm job. Each reponse allows the f/e to check the
# specific transorm job is finished (/checktransformjob) which will be called (say) every 1 sec. from f/e
# with a spinning wheel, then when pa_job_mgr has finished it will return the transformed thumb
@app.route("/transform", methods=["POST"])
@login_required
def transform():
id = request.form['id']
amt = request.form['amt']
print( f"transform called with id={id}, amt={amt}")
jex=[]
for el in request.form:
jex.append( JobExtra( name=f"{el}", value=request.form[el] ) )
job=NewJob( "transform_image", 0, None, jex )
resp={}
resp['job_id']=job.id
return resp
################################################################################
# /checktransformjob -> URL that is called repeatedly by front-end waiting for the
# b/e to finish the transform job. Once done, the new / now
# transformed image's thumbnail is returned so the f/e can
# update with it
################################################################################
@app.route("/checktransformjob", methods=["POST"])
@login_required
def checktransformjob():
job_id = request.form['job_id']
job = Job.query.get(job_id)
resp={}
resp['finished']=False
if job.pa_job_state == 'Completed':
id=[jex.value for jex in job.extra if jex.name == "id"][0]
e=Entry.query.join(File).filter(Entry.id==id).first()
resp['thumbnail']=e.file_details.thumbnail
resp['finished']=True
return resp
################################################################################
# /include -> return contents on /include and does not need a login, so we
# can get the icon, and potentially any js, bootstrap, etc. needed for the login page
################################################################################
@app.route("/internal/<path:filename>")
def internal(filename):
return send_from_directory("internal/", filename)
################################################################################
# /static -> returns the contents of any file referenced inside /static.
# we create/use symlinks in static/ to reference the images to show
################################################################################
@app.route("/static/<filename>")
@login_required
def custom_static(filename):
return send_from_directory("static/", filename)
###############################################################################
# This func creates a new filter in jinja2 to test to see if the Dir being
# checked, is a top-level folder of 'OPT.cwd'
################################################################################
@app.template_filter('TopLevelFolderOf')
def _jinja2_filter_toplevelfolderof(path, cwd):
if os.path.dirname(path) == cwd:
return True
else:
return False
###############################################################################
# This func creates a new filter in jinja2 to test to hand back the parent path
# from a given path
################################################################################
@app.template_filter('ParentPath')
def _jinja2_filter_parentpath(path):
return os.path.dirname(path)
###############################################################################
# route to allow the Move Dialog Box to pass a date (YYYYMMDD) and returns a
# json? list of existing dir names that could be near it in time. Starting
# simple, by using YYYYMM-1, YYYYMM, YYYYMM+1 dirs
###############################################################################
@app.route("/getexistingpaths/<dt>", methods=["POST"])
@login_required
def GetExistingPathsAsDiv(dt):
dir_ft=FileType.query.filter(FileType.name=='Directory').first()
dirs_arr=[]
for delta in range(-7, 8):
try:
new_dtime=datetime.datetime.strptime(dt, "%Y%m%d") + datetime.timedelta(days=delta)
except:
# this is not a date, so we cant work out possible dirs, just
# return an empty set
return "[]"
new_dt=new_dtime.strftime('%Y%m%d')
dirs_arr+=Dir.query.distinct(Dir.rel_path).filter(Dir.rel_path.ilike('%'+new_dt+'%')).all();
dirs_arr+=Dir.query.distinct(Dir.rel_path).join(EntryDirLink).join(Entry).filter(Entry.type_id!=dir_ft.id).filter(Entry.name.ilike('%'+new_dt+'%')).all()
# remove duplicates from array
dirs = set(dirs_arr)
# turn DB output into json and return it to the f/e
ret='[ '
first_dir=1
for dir in dirs:
print( f"process dir matching for {new_dt} => {dir}")
if not first_dir:
ret +=", "
bits=dir.rel_path.split('-')
ret+= '{ '
ret+= '"prefix":"' + bits[0] + '", '
if len(bits)>1:
ret+= '"suffix":"' + bits[1] + '"'
else:
ret+= '"suffix":"''"'
ret+= ' } '
first_dir=0
ret+= ' ]'
return ret