Browse Source

Initial commit

master
cinnaboot 7 years ago
commit
d19a86ffb8
  1. 2
      .gitignore
  2. 34
      run.sh
  3. 51
      video_metadata/__init__.py
  4. 40
      video_metadata/db.py
  5. 82
      video_metadata/schema.sql
  6. 28
      video_metadata/static/style.css
  7. 22
      video_metadata/templates/base.html
  8. 13
      video_metadata/templates/file_listing/index.html
  9. 8
      video_metadata/templates/index.html
  10. 35
      video_metadata/templates/tmdb/query.html
  11. 60
      video_metadata/util.py
  12. 12
      video_metadata/views/file_listing.py
  13. 167
      video_metadata/views/tmdb.py

2
.gitignore vendored

@ -0,0 +1,2 @@
instance/*
__pycache__/

34
run.sh

@ -0,0 +1,34 @@
#!/bin/bash
export FLASK_APP=video_metadata
export FLASK_ENV=development
show_help() {
echo "Usage: $0 [OPTION]"
echo "Without options, run the flask builtin server."
echo ""
echo "-h, --help display this help and exit"
echo " --initdb (re-)initialize the the local sqlite DB from schema"
echo " --shell enter interactive python session with flask context"
echo ""
}
case $1 in
-h|--help)
show_help
;;
--initdb)
flask init-db
;;
--shell)
flask shell
;;
"")
flask run
;;
*)
show_help
;;
esac

51
video_metadata/__init__.py

@ -0,0 +1,51 @@
import json
import os
from flask import Flask, render_template
def create_app():
# create and configure the app
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY = 'dev',
DATABASE = os.path.join(app.instance_path, 'video_metadata.sqlite'),
MOVIE_DIR = os.path.join(app.instance_path, 'movie_dir'),
TMDB_API_KEY = parse_API_key(app, os.path.join(app.instance_path, 'API_KEY.json')),
)
# ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
from . import db
db.init_app(app)
from video_metadata.views import file_listing
app.register_blueprint(file_listing.bp)
from video_metadata.views import tmdb
app.register_blueprint(tmdb.bp)
app.add_url_rule('/', 'index', index)
return app
def index():
return render_template('index.html')
def parse_API_key(app, path):
try:
fd = open(path)
except OSError as e:
app.logger.error('error opening path: %s, %s', path, e.strerror)
return ''
j = json.load(fd)
fd.close()
return j['key']

40
video_metadata/db.py

@ -0,0 +1,40 @@
import sqlite3
import click
from flask import current_app, g
from flask.cli import with_appcontext
def get_db():
if 'db' not in g:
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 init_app(app):
app.teardown_appcontext(close_db)
app.cli.add_command(init_db_command)
def init_db():
db = get_db()
with current_app.open_resource('schema.sql') as f:
db.executescript(f.read().decode('utf8'))
@click.command('init-db')
@with_appcontext
def init_db_command():
"""Clear the existing data and create new tables."""
init_db()
click.echo('Initialized the database.')

82
video_metadata/schema.sql

@ -0,0 +1,82 @@
/*
* This file is part of video_metadata.
*
* video_metadata is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* video_metadata is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with video_metadata. If not, see <https://www.gnu.org/licenses/>.
*/
DROP TABLE IF EXISTS movie_queries;
CREATE TABLE movie_queries(
query_id integer primary key autoincrement,
query text,
year integer
);
DROP TABLE IF EXISTS movie_details;
CREATE TABLE movie_details(
movie_id primary key not null,
title text,
overview text,
release_date date,
poster_path text
);
DROP TABLE IF EXISTS movie_query_mapping;
CREATE TABLE movie_query_mapping(
query text,
query_id int,
movie_id int,
foreign key(query_id) references movie_queries(query_id),
foreign key(movie_id) references movie_details(movie_id)
);
DROP TABLE IF EXISTS movie_genres;
CREATE TABLE movie_genres(
genre_id integer primary key not null,
genre_name
);
DROP TABLE IF EXISTS movie_genre_mapping;
CREATE TABLE movie_genre_mapping(
genre_id integer,
movie_id integer,
foreign key(genre_id) references movie_genres(genre_id),
foreign key(movie_id) references movie_details(movie_id)
);
DROP TABLE IF EXISTS actor_details;
CREATE TABLE actor_details(
actor_id integer primary key not null,
name text,
date_of_birth date,
poster_path text
);
DROP TABLE IF EXISTS actor_movie_mapping;
CREATE TABLE actor_movie_mapping(
movie_id integer,
actor_id integer,
character_name text,
foreign key(movie_id) references movie_details(movie_id),
foreign key(actor_id) references actor_details(actor_id)
);
-- TODO: tv shows
DROP TABLE IF EXISTS tv_queries;
CREATE TABLE tv_queries(
query text,
year integer
);

28
video_metadata/static/style.css

@ -0,0 +1,28 @@
html { font-family: sans-serif; background: #eee; padding: 1rem; }
body { max-width: 960px; margin: 0 auto; background: white; }
h1 { font-family: serif; color: #377ba8; margin: 1rem 0; }
a { color: #377ba8; }
hr { border: none; border-top: 1px solid lightgray; }
nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; }
nav h1 { flex: auto; margin: 0; }
nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; }
nav ul { display: flex; list-style: none; margin: 0; padding: 0; }
nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; }
.content { padding: 0 1rem 1rem; }
.content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; }
.content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; }
.flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; }
.post > header { display: flex; align-items: flex-end; font-size: 0.85em; }
.post > header > div:first-of-type { flex: auto; }
.post > header h1 { font-size: 1.5em; margin-bottom: 0; }
.post .about { color: slategray; font-style: italic; }
.post .body { white-space: pre-line; }
.content:last-child { margin-bottom: 0; }
.content form { margin: 1em 0; display: flex; flex-direction: column; }
.content label { font-weight: bold; margin-bottom: 0.5em; }
.content input, .content textarea { margin-bottom: 1em; }
.content textarea { min-height: 12em; resize: vertical; }
input { color: #333; background-color: #eee; }
input.danger { color: #cc2f2e; }
input[type=submit] { align-self: start; min-width: 10em; }
.debug { background: lightgrey; padding: 1em; overflow: auto; }

22
video_metadata/templates/base.html

@ -0,0 +1,22 @@
<!doctype html>
<title>{% block title %}{% endblock %} - video_metadata</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<nav>
<h1>video_metadata</h1>
<ul>
<li><a href="{{ url_for('index') }}">Home</a>
<li><a href="{{ url_for('file_listing.index') }}">File Listing</a>
<li><a href="{{ url_for('tmdb.query_movie') }}">Query</a>
</ul>
</nav>
<section class="content">
<header>
{% block header %}{% endblock %}
</header>
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% block content %}{% endblock %}
</section>

13
video_metadata/templates/file_listing/index.html

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}File Listing{% endblock %}</h1>
{% endblock %}
{% block content %}
<ul>
{% for file in filenames %}
<li><span>! </span><a href="{{ url_for('tmdb.query_movie') }}?query={{ file|urlencode() }}">{{ file }}</a></li>
{% endfor %}
</ul>
{% endblock %}

8
video_metadata/templates/index.html

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}INDEX PAGE!{% endblock %}</h1>
{% endblock %}
{% block content %}
{% endblock %}

35
video_metadata/templates/tmdb/query.html

@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}TMDB Query{% endblock %}</h1>
{% endblock %}
{% block content %}
{% if view_data.raw_query_str == '' %}
<form method="get">
<label for="query">Search for movie</label>
<input name="query" id="query">
<input type="submit" value="Search">
</form>
{% endif %}
<div id="search_results">
<h2>Query Results</h2>
{% for r in view_data.response_object.results %}
<div>
<h3>{{ r.original_title }}</h3>
<ul>
<li>Release Date: {{ r.release_date }}</li>
<li>Description:
<p>{{ r.overview }}</p>
</li>
</ul>
</div>
{% endfor %}
</div>
<div class="debug">
<h3>Debug Info</h3>
<pre>{{ debug_str }}</pre>
</div>
{% endblock %}

60
video_metadata/util.py

@ -0,0 +1,60 @@
# NOTE: modified from here: https://code.activestate.com/recipes/137951-dump-all-the-attributes-of-an-object/
# to only print attributes
def dumpObjAttr(obj):
import types
MethodWrapperType = type(object().__hash__)
objclass = getattr(obj, '__class__')
attrs = []
for slot in dir(obj):
attr = getattr(obj, slot)
if slot == '__class__':
objclass = attr.__name__
elif slot == '__doc__':
pass
elif slot == '__module__':
pass
elif slot == '__dict__':
pass
elif slot == '__weakref__':
pass
elif (isinstance(attr, types.BuiltinMethodType) or
isinstance(attr, MethodWrapperType)):
pass
elif (isinstance(attr, types.MethodType) or
isinstance(attr, types.FunctionType)):
pass
else:
attrs.append( (slot, attr) )
sOut = objclass + ':'
for key, value in attrs:
# NOTE: pretty print for long lists
if isinstance(value, list) or isinstance(value, tuple):
sOut += '\n\t' + key + ':'
sOut += dumpList(value, 2)
else:
sOut += '\n\t' + key + ': ' + str(value)
return sOut
def dumpList(value, indent):
s = ''
if len(value) > 0:
if isinstance(value[0], tuple):
for k, v in value:
s += '\n' + (indent * '\t') + k + ': ' + str(v)
elif isinstance(value, list):
for item in value:
s += '\n' + (indent * '\t') + str(item)
else:
pass
return s
def getFunctionName():
import sys
return sys._getframe(1).f_code.co_name

12
video_metadata/views/file_listing.py

@ -0,0 +1,12 @@
import os
from flask import Blueprint, current_app, render_template
bp = Blueprint('file_listing', __name__)
@bp.route('/file_listing')
def index():
video_path = current_app.config['MOVIE_DIR']
filenames = os.listdir(video_path)
return render_template('file_listing/index.html', filenames=filenames)

167
video_metadata/views/tmdb.py

@ -0,0 +1,167 @@
import cgi
from contextlib import closing
from datetime import date
from enum import Enum
import json
from pprint import pprint
import re
import ssl
from urllib.error import URLError, HTTPError
import urllib.parse
import urllib.request
from flask import Blueprint, current_app, flash, request, render_template
from .. import util
####################
# constants
bp = Blueprint('tmdb', __name__, url_prefix='/tmdb')
TMDB_URL = 'https://api.themoviedb.org'
TMDB_SEARCH_MOVIE = '/3/search/movie'
TMDB_SEARCH_TV = '/3/search/tv'
####################
# data structures
class query_type(Enum):
UNKNOWN = 0
MOVIE = 1
TV_SHOW = 2
class query_data(object):
def __init__(self):
self.errors = []
self.full_url = ''
self.movie_year = 0
self.parsed_query_str = ''
self.query_params = {}
self.query_type = query_type.UNKNOWN
self.raw_query_str = ''
self.response_body = ''
self.response_headers = []
self.response_object = {}
self.response_status = 0
self.tmdb_api_key = ''
####################
# flask routes
@bp.route('/query_movie')
def query_movie():
view_data = query_data()
view_data.tmdb_api_key = current_app.config['TMDB_API_KEY']
# NOTE: query string can be submitted from a link or form input
if 'query' in request.form:
view_data.raw_query_str = request.form['query']
elif 'query' in request.args:
view_data.raw_query_str = request.args['query']
if parseQueryStr(view_data) and makeQuery(view_data):
try:
p = view_data.response_object = json.loads(view_data.response_body)
pprint(p)
except json.JSONDecodeError as e:
view_data.errors.append(e)
else:
# display errors
pass
if view_data.errors:
flash(view_data.errors)
return render_template(
'tmdb/query.html',
view_data=view_data,
debug_str=util.dumpObjAttr(view_data)
)
###################
# helpers
# TODO: need a way to correctly parse titles like: "2001: A Space Odyssey (1968)"
# with legitimate dates in the title
def parseQueryStr(query_obj):
if not query_obj.raw_query_str:
query_obj.errors.append('%s: raw query string is empty' % util.getFunctionName())
return False
s = query_obj.raw_query_str.replace('.', ' ').strip()
year = None
current_year = date.today().year
# NOTE: find all 4 digit numbers to match the year of queries with 4 digit numbers in the title
# ex) "2036.Origin.Unknown.2018"
regex = re.compile('[0-9]{4}')
for m in regex.finditer(s):
i = int(m[0])
if i > 1878 and i <= current_year:
query_obj.parsed_query_str = s[0:m.start()].strip() # NOTE: slice query string to year match
query_obj.movie_year = i
query_obj.query_type = query_type.MOVIE
break
# TODO: parse tv show titles
if query_obj.query_type == query_type.MOVIE:
return True
else:
query_obj.errors.append('%s: unable to determine query type' % util.getFunctionName())
return False
def makeQuery(query_obj):
if query_obj.query_type == query_type.MOVIE:
search_part = TMDB_SEARCH_MOVIE
elif query_obj.query_type == query_type.TV_SHOW:
search_part = TMDB_SEARCH_TV
else:
query_obj.errors.append('%s(), query type is not set' % util.getFunctionName())
return False
request_headers = {"Accept": "text/html,application/xhtml+xml,application/xml"}
query_obj.query_params = urllib.parse.urlencode(
{
'api_key': query_obj.tmdb_api_key,
'query': query_obj.parsed_query_str,
'year': query_obj.movie_year,
}
)
query_obj.full_url = TMDB_URL + search_part + '?' + query_obj.query_params
context = ssl.create_default_context()
req = urllib.request.Request(query_obj.full_url, None, request_headers)
ct_header = ''
try:
response = urllib.request.urlopen(req, context=context)
except HTTPError as e:
query_obj.errors.append(e)
return False
except URLError as e:
query_obj.errors.append(e)
return False
for k, v in response.info().items():
query_obj.response_headers.append((k, v))
if k == 'Content-Type':
ct_header = str(k + ':' + v)
query_obj.response_status = response.status
if 'charset' in ct_header:
charset = cgi.parse_header(ct_header)[1]['charset']
query_obj.response_body = response.read().decode(charset)
else:
query_obj.errors.append('%s(), no charset key in ct_header' % util.getFunctionName())
query_obj.response_body = response.read()
return False
return True
Loading…
Cancel
Save