Merge pull request #71 from noonat/sphinx-docs
gh-70 Adding Sphinx docs
This commit is contained in:
		
						commit
						003327916d
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -3,4 +3,5 @@
 | 
			
		||||
**/config.ini
 | 
			
		||||
**/node_modules/**
 | 
			
		||||
dist/**
 | 
			
		||||
docs/_build
 | 
			
		||||
build/**
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										192
									
								
								docs/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								docs/Makefile
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										291
									
								
								docs/conf.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										103
									
								
								docs/index.rst
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										5
									
								
								docs/requirements.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
LatLon
 | 
			
		||||
docopt
 | 
			
		||||
requests
 | 
			
		||||
mock
 | 
			
		||||
sphinx
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user