gh-70 Adding Sphinx docs

This commit is contained in:
Nathan Ostgard 2016-01-08 14:49:06 -08:00
parent 6c96e73f23
commit cfe82012b0
16 changed files with 1010 additions and 300 deletions

1
.gitignore vendored
View File

@ -3,4 +3,5 @@
**/config.ini
**/node_modules/**
dist/**
docs/_build
build/**

192
docs/Makefile Normal file
View File

@ -0,0 +1,192 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Elodie.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Elodie.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Elodie"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Elodie"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

291
docs/conf.py Normal file
View File

@ -0,0 +1,291 @@
# -*- coding: utf-8 -*-
#
# Elodie documentation build configuration file, created by
# sphinx-quickstart on Fri Jan 8 14:42:49 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import os
import shlex
import sys
import mock
# Add the parent folder to the Python path so Sphinx can import elodie modules.
sys.path.insert(0, os.path.abspath('..'))
# Mock out the pyexiv2 module so we don't have to install it when we build
# docs on ReadTheDocs.
sys.modules['pyexiv2'] = mock.Mock()
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Elodie'
copyright = u'2016, Jaisen Mathai'
author = u'Jaisen Mathai'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'0.1.0'
# The full version, including alpha/beta/rc tags.
release = u'0.1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
# html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
#html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# Now only 'ja' uses this config value
#html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'Elodiedoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Elodie.tex', u'Elodie Documentation',
u'Jaisen Mathai', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'elodie', u'Elodie Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Elodie', u'Elodie Documentation',
author, 'Elodie', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False

103
docs/index.rst Normal file
View File

@ -0,0 +1,103 @@
.. toctree::
:hidden:
self
Hello, I'm Elodie
=================
*~~ Your Personal EXIF-based Photo, Video and Audio Assistant ~~*
.. image:: ../creative/logo@300x.png
:align: center
I work tirelessly to make sure your photos are always sorted and organized so
you can focus on more important things. By photos I mean JPEG, DNG, NEF and
common video and audio files.
You don't love me yet but you will.
I only do 3 things.
- Firstly I organize your existing collection of photos.
- Second I help make it easy for all the photos you haven't taken yet to flow
into the exact location they belong.
- Third but not least I promise to do all this without a yucky proprietary
database that some friends of mine use.
You can find out more information about me on `GitHub`_.
.. _GitHub: https://github.com/jmathai/elodie
API Documentation
=================
This documentation is generated from the Python code.
.. contents:: Modules
:local:
elodie.media
------------
.. automodule:: elodie.media.media
:members:
.. automodule:: elodie.media.audio
:members:
.. automodule:: elodie.media.photo
:members:
.. automodule:: elodie.media.video
:members:
elodie.arguments
----------------
.. automodule:: elodie.arguments
:members:
elodie.constants
----------------
.. automodule:: elodie.constants
:members:
elodie.dependencies
-------------------
.. automodule:: elodie.dependencies
:members:
elodie.filesystem
-----------------
.. automodule:: elodie.filesystem
:members:
elodie.geolocation
------------------
.. automodule:: elodie.geolocation
:members:
elodie.localstorage
-------------------
.. automodule:: elodie.localstorage
:members:
elodie.plist_parser
-------------------
.. automodule:: elodie.plist_parser
:members:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

5
docs/requirements.txt Normal file
View File

@ -0,0 +1,5 @@
LatLon
docopt
requests
mock
sphinx

View File

@ -1,11 +1,22 @@
"""
Command line argument parsing for helper scripts.
"""
import getopt
import sys
from re import sub
def parse(argv, options, long_options, usage):
"""Parse command line arguments.
:param list(str) argv: Arguments passed to the program.
:param str options: String of characters for allowed short options.
:param list(str) long_options: List of strings of allowed long options.
:param str usage: Help text, to print in the case of an error or when
the user asks for it.
:returns: dict
"""
try:
opts, args = getopt.getopt(argv, options, long_options)
except getopt.GetoptError:

View File

@ -1,8 +1,23 @@
"""
Settings used by Elodie.
"""
from os import path
#: If True, debug messages will be printed.
debug = True
#: Directory in which to store Elodie settings.
application_directory = '{}/.elodie'.format(path.expanduser('~'))
#: File in which to store details about media Elodie has seen.
hash_db = '{}/hash.json'.format(application_directory)
#: File in which to store geolocation details about media Elodie has seen.
location_db = '{}/location.json'.format(application_directory)
#: Elodie installation directory.
script_directory = path.dirname(path.dirname(path.abspath(__file__)))
#: Path to Elodie's ExifTool config file.
exiftool_config = '%s/configs/ExifTool_config' % script_directory

View File

@ -1,10 +1,14 @@
"""Helpers for checking external dependencies."""
"""
Helpers for checking for an interacting with external dependencies. These are
things that Elodie requires, but aren't installed automatically for the user.
"""
import os
import sys
from distutils.spawn import find_executable
#: Error to print when exiftool can't be found.
EXIFTOOL_ERROR = u"""
It looks like you don't have exiftool installed, which Elodie requires.
Please take a look at the installation steps in the readme:
@ -12,6 +16,7 @@ Please take a look at the installation steps in the readme:
https://github.com/jmathai/elodie#install-everything-you-need
""".lstrip()
#: Template for the error to print when pyexiv2 can't be found.
PYEXIV2_ERROR = u"""
{error_class_name}: {error}
@ -27,7 +32,7 @@ def get_exiftool():
We wrap this since we call it in a few places and we do a fallback.
@returns, None or string
:returns: str or None
"""
path = find_executable('exiftool')
# If exiftool wasn't found we try to brute force the homebrew location
@ -44,7 +49,7 @@ def verify_dependencies():
Prints a message to stderr and returns False if any dependencies are
missing.
@returns, bool
:returns: bool
"""
exiftool = get_exiftool()
if exiftool is None:

View File

@ -1,7 +1,9 @@
"""
Author: Jaisen Mathai <jaisen@jmathai.com>
General file system methods
General file system methods.
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
"""
import os
import re
import shutil
@ -12,14 +14,17 @@ from elodie import constants
from elodie.localstorage import Db
class FileSystem:
"""
Create a directory if it does not already exist..
class FileSystem(object):
"""A class for interacting with the file system."""
@param, directory_name, string, A fully qualified path of the
directory to create.
"""
def create_directory(self, directory_path):
"""Create a directory if it does not already exist.
:param str directory_name: A fully qualified path of the
to create.
:returns: bool
"""
try:
if os.path.exists(directory_path):
return True
@ -32,15 +37,15 @@ class FileSystem:
return False
"""
Delete a directory only if it's empty.
Instead of checking first using `len([name for name in
os.listdir(directory_path)]) == 0` we catch the OSError exception.
def delete_directory_if_empty(self, directory_path):
"""Delete a directory only if it's empty.
@param, directory_name, string, A fully qualified path of the directory
Instead of checking first using `len([name for name in
os.listdir(directory_path)]) == 0`, we catch the OSError exception.
:param str directory_name: A fully qualified path of the directory
to delete.
"""
def delete_directory_if_empty(self, directory_path):
try:
os.rmdir(directory_path)
return True
@ -49,13 +54,12 @@ class FileSystem:
return False
"""
Recursively get all files which match a path and extension.
@param, path, string, Path to start recursive file listing
@param, extensions, tuple, File extensions to include (whitelist)
"""
def get_all_files(self, path, extensions=None):
"""Recursively get all files which match a path and extension.
:param str path string: Path to start recursive file listing
:param tuple(str) extensions: File extensions to include (whitelist)
"""
files = []
for dirname, dirnames, filenames in os.walk(path):
# print path to all filenames.
@ -67,25 +71,25 @@ class FileSystem:
files.append('%s/%s' % (dirname, filename))
return files
"""
Get the current working directory
@returns, string
"""
def get_current_directory(self):
"""Get the current working directory.
:returns: str
"""
return os.getcwd()
"""
Generate file name for a photo or video using its metadata.
We use an ISO8601-like format for the file name prefix.
Instead of colons as the separator for hours, minutes and seconds we use a
hyphen.
def get_file_name(self, media):
"""Generate file name for a photo or video using its metadata.
We use an ISO8601-like format for the file name prefix. Instead of
colons as the separator for hours, minutes and seconds we use a hyphen.
https://en.wikipedia.org/wiki/ISO_8601#General_principles
@param, media, Photo|Video, A Photo or Video instance
@returns, string or None for non-photo or non-videos
:param media: A Photo or Video instance
:type media: :class:`~elodie.media.photo.Photo` or
:class:`~elodie.media.video.Video`
:returns: str or None for non-photo or non-videos
"""
def get_file_name(self, media):
if(not media.is_valid()):
return None
@ -124,22 +128,20 @@ class FileSystem:
metadata['extension'])
return file_name.lower()
"""
Get date based folder name.
@param, time_obj, time, Time object to be used to determine folder name.
@returns, string
"""
def get_folder_name_by_date(self, time_obj):
"""Get date based folder name.
:param time time_obj: Time object to be used to determine folder name.
:returns: str
"""
return time.strftime('%Y-%m-%b', time_obj)
"""
Get folder path by various parameters.
@param, time_obj, time, Time object to be used to determine folder name.
@returns, string
"""
def get_folder_path(self, metadata):
"""Get folder path by various parameters.
:param time time_obj: Time object to be used to determine folder name.
:returns: str
"""
path = []
if(metadata['date_taken'] is not None):
path.append(time.strftime('%Y-%m-%b', metadata['date_taken']))
@ -212,11 +214,13 @@ class FileSystem:
return dest_path
"""
Set the modification time on the file based on the file path.
Noop if the path doesn't match the format YYYY-MM/DD-IMG_0001.JPG.
"""
def set_date_from_path_video(self, video):
"""Set the modification time on the file based on the file path.
Noop if the path doesn't match the format YYYY-MM/DD-IMG_0001.JPG.
:param elodie.media.video.Video video: An instance of Video.
"""
date_taken = None
video_file_path = video.get_file_path()

View File

@ -1,3 +1,5 @@
"""Look up geolocation information for media objects."""
from os import path
from ConfigParser import ConfigParser
import fractions
@ -11,14 +13,18 @@ from elodie.localstorage import Db
class Fraction(fractions.Fraction):
"""Only create Fractions from floats.
Should be compatible with Python 2.6, though untested.
>>> Fraction(0.3)
Fraction(3, 10)
>>> Fraction(1.1)
Fraction(11, 10)
"""
def __new__(cls, value, ignore=None):
"""Should be compatible with Python 2.6, though untested."""
return fractions.Fraction.from_float(value).limit_denominator(99999)

View File

@ -1,3 +1,7 @@
"""
Methods for interacting with information Elodie caches about stored media.
"""
import hashlib
import json
from math import radians, cos, sqrt
@ -8,6 +12,9 @@ from elodie import constants
class Db(object):
"""A class for interacting with the JSON files created by Elodie."""
def __init__(self):
# verify that the application directory (~/.elodie) exists,
# else create it
@ -47,26 +54,49 @@ class Db(object):
pass
def add_hash(self, key, value, write=False):
"""Add a hash to the hash db.
:param str key:
:param str value:
:param bool write: If true, write the hash db to disk.
"""
self.hash_db[key] = value
if(write is True):
self.update_hash_db()
def check_hash(self, key):
"""Check whether a hash is present for the given key.
:param str key:
:returns: bool
"""
return key in self.hash_db
def get_hash(self, key):
"""Get the hash value for a given key.
:param str key:
:returns: str or None
"""
if(self.check_hash(key) is True):
return self.hash_db[key]
return None
def update_hash_db(self):
"""Write the hash db to disk."""
with open(constants.hash_db, 'w') as f:
json.dump(self.hash_db, f)
"""
http://stackoverflow.com/a/3431835/1318758
"""
def checksum(self, file_path, blocksize=65536):
"""Create a hash value for the given file.
See http://stackoverflow.com/a/3431835/1318758.
:param str file_path: Path to the file to create a hash for.
:param int blocksize: Read blocks of this size from the file when
creating the hash.
:returns: str or None
"""
hasher = hashlib.sha256()
with open(file_path, 'r') as f:
buf = f.read(blocksize)
@ -79,14 +109,21 @@ class Db(object):
# Location database
# Currently quite simple just a list of long/lat pairs with a name
# If it gets many entryes a lookup might takt to long and a better
# If it gets many entries a lookup might take too long and a better
# structure might be needed. Some speed up ideas:
# - Sort it and inter-half method can be used
# - Use integer part of long or lat as key to get a lower search list
# - Cache a smal number of lookups, photos is likey to be taken i clusters
# around a spot during import.
# - Cache a small number of lookups, photos are likely to be taken in
# clusters around a spot during import.
def add_location(self, latitude, longitude, place, write=False):
"""Add a location to the database.
:param float latitude: Latitude of the location.
:param float longitude: Longitude of the location.
:param str place: Name for the location.
:param bool write: If true, write the location db to disk.
"""
data = {}
data['lat'] = latitude
data['long'] = longitude
@ -96,10 +133,18 @@ class Db(object):
self.update_location_db()
def get_location_name(self, latitude, longitude, threshold_m):
"""Find a name for a location in the database.
:param float latitude: Latitude of the location.
:param float longitude: Longitude of the location.
:param int threshold_m: Location in the database must be this close to
the given latitude and longitude.
:returns: str, or None if a matching location couldn't be found.
"""
last_d = sys.maxint
name = None
for data in self.location_db:
# As threshold is quite smal use simple math
# As threshold is quite small use simple math
# From http://stackoverflow.com/questions/15736995/how-can-i-quickly-estimate-the-distance-between-two-latitude-longitude-points # noqa
# convert decimal degrees to radians
@ -120,6 +165,11 @@ class Db(object):
return name
def get_location_coordinates(self, name):
"""Get the latitude and longitude for a location.
:param str name: Name of the location.
:returns: tuple(float), or None if the location wasn't in the database.
"""
for data in self.location_db:
if data['name'] == name:
return (data['lat'], data['long'])
@ -127,5 +177,6 @@ class Db(object):
return None
def update_location_db(self):
"""Write the location db to disk."""
with open(constants.location_db, 'w') as f:
json.dump(self.location_db, f)

View File

@ -1,18 +1,25 @@
"""
Author: Jaisen Mathai <jaisen@jmathai.com>
Audio package that handles all audio operations
Inherits from Video package
The audio module contains classes specifically for dealing with audio files.
The :class:`Audio` class inherits from the :class:`~elodie.media.video.Video`
class.
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
"""
from video import Video
class Audio(Video):
"""An audio object.
:param str source: The fully qualified path to the audio file.
"""
__name__ = 'Audio'
#: Valid extensions for audio files.
extensions = ('m4a',)
"""
@param, source, string, The fully qualified path to the audio file
"""
def __init__(self, source=None):
super(Audio, self).__init__(source)

View File

@ -1,6 +1,12 @@
"""
Author: Jaisen Mathai <jaisen@jmathai.com>
Media package that's a parent class for media objects
The media module provides a base :class:`Media` class for all objects that
are tracked by Elodie. The Media class provides some base functionality used
by all the media types, but isn't itself used to represent anything. Its
sub-classes (:class:`~elodie.media.audio.Audio`,
:class:`~elodie.media.photo.Photo`, and :class:`~elodie.media.video.Video`)
are used to represent the actual files.
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
"""
# load modules
@ -15,12 +21,14 @@ import subprocess
class Media(object):
# class / static variable accessible through get_valid_extensions()
"""The base class for all media objects.
:param str source: The fully qualified path to the video file.
"""
__name__ = 'Media'
"""
@param, source, string, The fully qualified path to the video file
"""
def __init__(self, source=None):
self.source = source
self.exif_map = {
@ -33,12 +41,11 @@ class Media(object):
self.exiftool_attributes = None
self.metadata = None
"""
Get album from EXIF
@returns, None or string
"""
def get_album(self):
"""Get album from EXIF
:returns: None or string
"""
if(not self.is_valid()):
return None
@ -48,29 +55,31 @@ class Media(object):
return exiftool_attributes['album']
"""
Get the full path to the video.
@returns string
"""
def get_file_path(self):
"""Get the full path to the video.
:returns: string
"""
return self.source
"""
Define is_valid to always return false.
This should be overridden in a child class.
"""
def is_valid(self):
"""The default is_valid() always returns false.
This should be overridden in a child class to return true if the
source is valid, and false otherwise.
:returns: bool
"""
return False
"""
Read EXIF from a photo file.
We store the result in a member variable so we can call get_exif() often
without performance degredation
@returns, list or none for a non-photo file
"""
def get_exif(self):
"""Read EXIF from a photo file.
We store the result in a member variable so we can call get_exif()
often without performance degredation.
:returns: list or none for a non-photo file
"""
if(not self.is_valid()):
return None
@ -84,6 +93,10 @@ class Media(object):
return self.exif
def get_exiftool_attributes(self):
"""Get attributes for the media object from exiftool.
:returns: dict, or False if exiftool was not available.
"""
if(self.exiftool_attributes is not None):
return self.exiftool_attributes
@ -122,25 +135,24 @@ class Media(object):
return self.exiftool_attributes
"""
Get the file extension as a lowercased string.
@returns, string or None for a non-video
"""
def get_extension(self):
"""Get the file extension as a lowercased string.
:returns: string or None for a non-video
"""
if(not self.is_valid()):
return None
source = self.source
return os.path.splitext(source)[1][1:].lower()
"""
Get a dictionary of metadata for a photo.
def get_metadata(self, update_cache=False):
"""Get a dictionary of metadata for a photo.
All keys will be present and have a value of None if not obtained.
@returns, dictionary or None for non-photo files
:returns: dict or None for non-photo files
"""
def get_metadata(self, update_cache=False):
if(not self.is_valid()):
return None
@ -163,12 +175,11 @@ class Media(object):
return self.metadata
"""
Get the mimetype of the file.
@returns, string or None for a non-video
"""
def get_mimetype(self):
"""Get the mimetype of the file.
:returns: str or None for a non-video
"""
if(not self.is_valid()):
return None
@ -179,12 +190,11 @@ class Media(object):
return mimetype[0]
"""
Get the title for a photo of video
@returns, string or None if no title is set or not a valid media type
"""
def get_title(self):
"""Get the title for a photo of video
:returns: str or None if no title is set or not a valid media type
"""
if(not self.is_valid()):
return None
@ -195,14 +205,12 @@ class Media(object):
return exiftool_attributes['title']
"""
Set album for a photo
@param, name, string, Name of album
@returns, boolean
"""
def set_album(self, name):
"""Set album for a photo
:param str name: Name of album
:returns: bool
"""
if(name is None):
return False
@ -252,27 +260,26 @@ class Media(object):
self.set_album(folder)
return True
"""
Specifically update the basename attribute in the metadata
dictionary for this instance.
This is used for when we update the EXIF title of a media file.
Since that determines the name of a file if we update the
title of a file more than once it appends to the file name.
I.e. 2015-12-31_00-00-00-my-first-title-my-second-title.jpg
@param, string, new_basename, New basename of file
(with the old title removed)
"""
def set_metadata_basename(self, new_basename):
"""Update the basename attribute in the metadata dict for this instance.
This is used for when we update the EXIF title of a media file. Since
that determines the name of a file if we update the title of a file
more than once it appends to the file name.
i.e. 2015-12-31_00-00-00-my-first-title-my-second-title.jpg
:param str new_basename: New basename of file (with the old title
removed).
"""
self.get_metadata()
self.metadata['base_name'] = new_basename
"""
Method to manually update attributes in metadata.
@params, named paramaters
"""
def set_metadata(self, **kwargs):
"""Method to manually update attributes in metadata.
:params dict kwargs: Named parameters to update.
"""
metadata = self.get_metadata()
for key in kwargs:
if(key in metadata):
@ -287,3 +294,11 @@ class Media(object):
return i(_file)
return None
@classmethod
def get_valid_extensions(cls):
"""Static method to access static extensions variable.
:returns: tuple(str)
"""
return cls.extensions

View File

@ -1,6 +1,8 @@
"""
Author: Jaisen Mathai <jaisen@jmathai.com>
Photo package that handles all photo operations
The photo module contains the :class:`Photo` class, which is used to track
image objects (JPG, DNG, etc.).
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
"""
import imghdr
@ -17,25 +19,28 @@ from elodie import geolocation
class Photo(Media):
"""A photo object.
:param str source: The fully qualified path to the photo file
"""
__name__ = 'Photo'
#: Valid extensions for photo files.
extensions = ('jpg', 'jpeg', 'nef', 'dng', 'gif')
"""
@param, source, string, The fully qualified path to the photo file
"""
def __init__(self, source=None):
super(Photo, self).__init__(source)
# We only want to parse EXIF once so we store it here
self.exif = None
"""
Get the duration of a photo in seconds.
Uses ffmpeg/ffprobe
@returns, string or None for a non-photo file
"""
def get_duration(self):
"""Get the duration of a photo in seconds. Uses ffmpeg/ffprobe.
:returns: str or None for a non-photo file
"""
if(not self.is_valid()):
return None
@ -53,12 +58,13 @@ class Photo(Media):
).group(1).replace('.', ':')
return None
"""
Get latitude or longitude of photo from EXIF
@returns, float or None if not present in EXIF or a non-photo file
"""
def get_coordinate(self, type='latitude'):
"""Get latitude or longitude of photo from EXIF
:param str type: Type of coordinate to get. Either "latitude" or
"longitude".
:returns: float or None if not present in EXIF or a non-photo file
"""
if(not self.is_valid()):
return None
@ -99,13 +105,13 @@ class Photo(Media):
except KeyError:
return None
"""
Get the date which the photo was taken.
def get_date_taken(self):
"""Get the date which the photo was taken.
The date value returned is defined by the min() of mtime and ctime.
@returns, time object or None for non-photo files or 0 timestamp
:returns: time object or None for non-photo files or 0 timestamp
"""
def get_date_taken(self):
if(not self.is_valid()):
return None
@ -135,13 +141,14 @@ class Photo(Media):
return time.gmtime(seconds_since_epoch)
"""
Check the file extension against valid file extensions as returned
by self.extensions
@returns, boolean
"""
def is_valid(self):
"""Check the file extension against valid file extensions.
The list of valid file extensions come from self.extensions. This
also checks whether the file is an image.
:returns: bool
"""
source = self.source
# gh-4 This checks if the source file is an image.
@ -151,14 +158,12 @@ class Photo(Media):
return os.path.splitext(source)[1][1:].lower() in self.extensions
"""
Set the date/time a photo was taken
@param, time, datetime, datetime object of when the photo was taken
@returns, boolean
"""
def set_date_taken(self, time):
"""Set the date/time a photo was taken.
:param datetime time: datetime object of when the photo was taken
:returns: bool
"""
if(time is None):
return False
@ -172,15 +177,13 @@ class Photo(Media):
exif_metadata.write()
return True
"""
Set lat/lon for a photo
@param, latitude, float, Latitude of the file
@param, longitude, float, Longitude of the file
@returns, boolean
"""
def set_location(self, latitude, longitude):
"""Set latitude and longitude for a photo.
:param float latitude: Latitude of the file
:param float longitude: Longitude of the file
:returns: bool
"""
if(latitude is None or longitude is None):
return False
@ -196,15 +199,12 @@ class Photo(Media):
exif_metadata.write()
return True
"""
Set title for a photo
@param, latitude, float, Latitude of the file
@param, longitude, float, Longitude of the file
@returns, boolean
"""
def set_title(self, title):
"""Set title for a photo.
:param str title: Title of the photo.
:returns: bool
"""
if(title is None):
return False
@ -216,12 +216,3 @@ class Photo(Media):
exif_metadata.write()
return True
"""
Static method to access static __valid_extensions variable.
@returns, tuple
"""
@classmethod
def get_valid_extensions(cls):
return cls.extensions

View File

@ -1,6 +1,8 @@
"""
Author: Jaisen Mathai <jaisen@jmathai.com>
Video package that handles all video operations
The video module contains the :class:`Video` class, which represents video
objects (AVI, MOV, etc.).
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
"""
# load modules
@ -21,24 +23,27 @@ from media import Media
class Video(Media):
"""A video object.
:param str source: The fully qualified path to the video file.
"""
__name__ = 'Video'
#: Valid extensions for video files.
extensions = ('avi', 'm4v', 'mov', 'mp4', '3gp')
"""
@param, source, string, The fully qualified path to the video file
@param, Audio, class or none, The Audio class if being extendted
by the Audio class
"""
def __init__(self, source=None):
super(Video, self).__init__(source)
"""
Get path to executable avmetareadwrite binary.
def get_avmetareadwrite(self):
"""Get path to executable avmetareadwrite binary.
We wrap this since we call it in a few places and we do a fallback.
@returns, None or string
:returns: None or string
"""
def get_avmetareadwrite(self):
avmetareadwrite = find_executable('avmetareadwrite')
if(avmetareadwrite is None):
avmetareadwrite = '/usr/bin/avmetareadwrite'
@ -47,12 +52,11 @@ class Video(Media):
return avmetareadwrite
"""
Get latitude or longitude of photo from EXIF
@returns, time object or None for non-video files or 0 timestamp
"""
def get_coordinate(self, type='latitude'):
"""Get latitude or longitude of photo from EXIF.
:returns: time object or None for non-video files or 0 timestamp
"""
exif_data = self.get_exif()
if(exif_data is None):
return None
@ -75,13 +79,13 @@ class Video(Media):
return decimal_degrees
"""
Get the date which the video was taken.
def get_date_taken(self):
"""Get the date which the video was taken.
The date value returned is defined by the min() of mtime and ctime.
@returns, time object or None for non-video files or 0 timestamp
:returns: time object or None for non-video files or 0 timestamp
"""
def get_date_taken(self):
if(not self.is_valid()):
return None
@ -114,13 +118,13 @@ class Video(Media):
return time.gmtime(seconds_since_epoch)
"""
Get the duration of a video in seconds.
Uses ffmpeg/ffprobe
@returns, string or None for a non-video file
"""
def get_duration(self):
"""Get the duration of a video in seconds.
This uses ffmpeg/ffprobe.
:returns: str or None for a non-video file
"""
if(not self.is_valid()):
return None
@ -138,14 +142,14 @@ class Video(Media):
).group(1).replace('.', ':')
return None
"""
Get exif data from video file.
Not all video files have exif and this currently relies on
the CLI exiftool program
@returns, string or None if exiftool is not found
"""
def get_exif(self):
"""Get exif data from video file.
Not all video files have exif and this currently relies on the CLI
exiftool program.
:returns: str or None if exiftool is not found
"""
exiftool = get_exiftool()
if(exiftool is None):
return None
@ -158,24 +162,24 @@ class Video(Media):
)
return process_output.stdout.read()
"""
Check the file extension against valid file extensions as
returned by self.extensions
@returns, boolean
"""
def is_valid(self):
"""Check the file extension against valid file extensions.
The list of valid file extensions come from self.extensions.
:returns: bool
"""
source = self.source
return os.path.splitext(source)[1][1:].lower() in self.extensions
def set_date_taken(self, date_taken_as_datetime):
"""
Set the date/time a photo was taken
@param, time, datetime, datetime object of when the photo was taken
@returns, boolean
:param datetime date_taken_as_datetime: datetime object of when the
video was recorded.
:returns: bool
"""
def set_date_taken(self, date_taken_as_datetime):
if(time is None):
return False
@ -193,56 +197,51 @@ class Video(Media):
return result
"""
Set lat/lon for a video
@param, latitude, float, Latitude of the file
@param, longitude, float, Longitude of the file
@returns, boolean
"""
def set_location(self, latitude, longitude):
"""
Set latitude and longitude for a video.
:param float latitude: Latitude of the file
:param float longitude: Longitude of the file
:returns: bool
"""
if(latitude is None or longitude is None):
return False
result = self.__update_using_plist(latitude=latitude, longitude=longitude) # noqa
return result
"""
Set title for a video
@param, title, string, Title for the file
@returns, boolean
"""
def set_title(self, title):
"""Set title for a video.
:param str title: Title for the file
:returns: bool
"""
if(title is None):
return False
result = self.__update_using_plist(title=title)
return result
"""
Updates video metadata using avmetareadwrite.
This method is a does a lot more than it should.
The general steps are...
1) Check if avmetareadwrite is installed
2) Export a plist file to a temporary location from the source file
3) Regex replace values in the plist file
4) Update the source file using the updated plist and save it to a
temporary location
5) Validate that the metadata in the updated temorary movie is valid
6) Copystat permission and time bits from the source file to the
temporary movie
7) Move the temporary file to overwrite the source file
@param, latitude, float, Latitude of the file
@param, longitude, float, Longitude of the file
@returns, boolean
"""
def __update_using_plist(self, **kwargs):
"""Updates video metadata using avmetareadwrite.
This method does a lot more than it should. The general steps are...
1. Check if avmetareadwrite is installed
2. Export a plist file to a temporary location from the source file
3. Regex replace values in the plist file
4. Update the source file using the updated plist and save it to a
temporary location
5. Validate that the metadata in the updated temorary movie is valid
6. Copystat permission and time bits from the source file to the
temporary movie
7. Move the temporary file to overwrite the source file
:param float latitude: Latitude of the file
:param float longitude: Longitude of the file
:returns: bool
"""
if(
'latitude' not in kwargs and
'longitude' not in kwargs and
@ -400,17 +399,13 @@ class Video(Media):
return True
"""
Static method to access static __valid_extensions variable.
@returns, tuple
"""
@classmethod
def get_valid_extensions(cls):
return cls.extensions
class Transcode(object):
# Constructor takes a video object as it's parameter
"""Constructor takes a video object as its parameter.
:param Video video: Video object.
"""
def __init__(self, video=None):
self.video = video

View File

@ -1,8 +1,7 @@
"""
Author: Jaisen Mathai <jaisen@jmathai.com>
Parse OS X plists.
Wraps standard lib plistlib (https://docs.python.org/3/library/plistlib.html)
Plist class to parse and interact with a plist file.
.. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com>
"""
# load modules
@ -12,15 +11,34 @@ import plistlib
class Plist(object):
"""Parse and interact with a plist file.
This class wraps the `plistlib module`_ from the standard library.
.. _plistlib module: https://docs.python.org/3/library/plistlib.html
:param str source: Source to read the plist from.
"""
def __init__(self, source):
if not path.isfile(source):
raise IOError('Could not load plist file %s' % source)
self.source = source
self.plist = plistlib.readPlist(self.source)
def update_key(self, key, value):
"""Update a value in the plist.
:param str key: Key to modify.
:param value: New value.
"""
self.plist[key] = value
def write_file(self, destination):
"""Save the plist.
:param destination: Write the plist here.
:type destination: str or file object
"""
plistlib.writePlist(self.plist, destination)