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 | **/config.ini | ||||||
| **/node_modules/** | **/node_modules/** | ||||||
| dist/** | dist/** | ||||||
|  | docs/_build | ||||||
| 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 getopt | ||||||
| import sys | import sys | ||||||
| from re import sub | from re import sub | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def parse(argv, options, long_options, usage): | 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: |     try: | ||||||
|         opts, args = getopt.getopt(argv, options, long_options) |         opts, args = getopt.getopt(argv, options, long_options) | ||||||
|     except getopt.GetoptError: |     except getopt.GetoptError: | ||||||
|  | |||||||
| @ -1,8 +1,23 @@ | |||||||
|  | """ | ||||||
|  | Settings used by Elodie. | ||||||
|  | """ | ||||||
|  | 
 | ||||||
| from os import path | from os import path | ||||||
| 
 | 
 | ||||||
|  | #: If True, debug messages will be printed. | ||||||
| debug = True | debug = True | ||||||
|  | 
 | ||||||
|  | #: Directory in which to store Elodie settings. | ||||||
| application_directory = '{}/.elodie'.format(path.expanduser('~')) | application_directory = '{}/.elodie'.format(path.expanduser('~')) | ||||||
|  | 
 | ||||||
|  | #: File in which to store details about media Elodie has seen. | ||||||
| hash_db = '{}/hash.json'.format(application_directory) | 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) | location_db = '{}/location.json'.format(application_directory) | ||||||
|  | 
 | ||||||
|  | #: Elodie installation directory. | ||||||
| script_directory = path.dirname(path.dirname(path.abspath(__file__))) | script_directory = path.dirname(path.dirname(path.abspath(__file__))) | ||||||
|  | 
 | ||||||
|  | #: Path to Elodie's ExifTool config file. | ||||||
| exiftool_config = '%s/configs/ExifTool_config' % script_directory | 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 os | ||||||
| import sys | import sys | ||||||
| from distutils.spawn import find_executable | from distutils.spawn import find_executable | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | #: Error to print when exiftool can't be found. | ||||||
| EXIFTOOL_ERROR = u""" | EXIFTOOL_ERROR = u""" | ||||||
| It looks like you don't have exiftool installed, which Elodie requires. | It looks like you don't have exiftool installed, which Elodie requires. | ||||||
| Please take a look at the installation steps in the readme: | 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 | https://github.com/jmathai/elodie#install-everything-you-need | ||||||
| """.lstrip() | """.lstrip() | ||||||
| 
 | 
 | ||||||
|  | #: Template for the error to print when pyexiv2 can't be found. | ||||||
| PYEXIV2_ERROR = u""" | PYEXIV2_ERROR = u""" | ||||||
| {error_class_name}: {error} | {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. |     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') |     path = find_executable('exiftool') | ||||||
|     # If exiftool wasn't found we try to brute force the homebrew location |     # 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 |     Prints a message to stderr and returns False if any dependencies are | ||||||
|     missing. |     missing. | ||||||
| 
 | 
 | ||||||
|     @returns, bool |     :returns: bool | ||||||
|     """ |     """ | ||||||
|     exiftool = get_exiftool() |     exiftool = get_exiftool() | ||||||
|     if exiftool is None: |     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 os | ||||||
| import re | import re | ||||||
| import shutil | import shutil | ||||||
| @ -12,14 +14,17 @@ from elodie import constants | |||||||
| from elodie.localstorage import Db | from elodie.localstorage import Db | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FileSystem: | class FileSystem(object): | ||||||
|     """ | 
 | ||||||
|     Create a directory if it does not already exist.. |     """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): |     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: |         try: | ||||||
|             if os.path.exists(directory_path): |             if os.path.exists(directory_path): | ||||||
|                 return True |                 return True | ||||||
| @ -32,15 +37,15 @@ class FileSystem: | |||||||
| 
 | 
 | ||||||
|         return False |         return False | ||||||
| 
 | 
 | ||||||
|     """ |     def delete_directory_if_empty(self, directory_path): | ||||||
|     Delete a directory only if it's empty. |         """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. |  | ||||||
| 
 | 
 | ||||||
|     @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. |             to delete. | ||||||
|         """ |         """ | ||||||
|     def delete_directory_if_empty(self, directory_path): |  | ||||||
|         try: |         try: | ||||||
|             os.rmdir(directory_path) |             os.rmdir(directory_path) | ||||||
|             return True |             return True | ||||||
| @ -49,13 +54,12 @@ class FileSystem: | |||||||
| 
 | 
 | ||||||
|         return False |         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): |     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 = [] |         files = [] | ||||||
|         for dirname, dirnames, filenames in os.walk(path): |         for dirname, dirnames, filenames in os.walk(path): | ||||||
|             # print path to all filenames. |             # print path to all filenames. | ||||||
| @ -67,25 +71,25 @@ class FileSystem: | |||||||
|                     files.append('%s/%s' % (dirname, filename)) |                     files.append('%s/%s' % (dirname, filename)) | ||||||
|         return files |         return files | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     Get the current working directory |  | ||||||
| 
 |  | ||||||
|     @returns, string |  | ||||||
|     """ |  | ||||||
|     def get_current_directory(self): |     def get_current_directory(self): | ||||||
|  |         """Get the current working directory. | ||||||
|  | 
 | ||||||
|  |         :returns: str | ||||||
|  |         """ | ||||||
|         return os.getcwd() |         return os.getcwd() | ||||||
| 
 | 
 | ||||||
|     """ |     def get_file_name(self, media): | ||||||
|     Generate file name for a photo or video using its metadata. |         """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 |         We use an ISO8601-like format for the file name prefix. Instead of | ||||||
|         hyphen. |         colons as the separator for hours, minutes and seconds we use a hyphen. | ||||||
|         https://en.wikipedia.org/wiki/ISO_8601#General_principles |         https://en.wikipedia.org/wiki/ISO_8601#General_principles | ||||||
| 
 | 
 | ||||||
|     @param, media, Photo|Video, A Photo or Video instance |         :param media: A Photo or Video instance | ||||||
|     @returns, string or None for non-photo or non-videos |         :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()): |         if(not media.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
| @ -124,22 +128,20 @@ class FileSystem: | |||||||
|             metadata['extension']) |             metadata['extension']) | ||||||
|         return file_name.lower() |         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): |     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) |         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): |     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 = [] |         path = [] | ||||||
|         if(metadata['date_taken'] is not None): |         if(metadata['date_taken'] is not None): | ||||||
|             path.append(time.strftime('%Y-%m-%b', metadata['date_taken'])) |             path.append(time.strftime('%Y-%m-%b', metadata['date_taken'])) | ||||||
| @ -212,11 +214,13 @@ class FileSystem: | |||||||
| 
 | 
 | ||||||
|         return dest_path |         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): |     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 |         date_taken = None | ||||||
| 
 | 
 | ||||||
|         video_file_path = video.get_file_path() |         video_file_path = video.get_file_path() | ||||||
|  | |||||||
| @ -1,3 +1,5 @@ | |||||||
|  | """Look up geolocation information for media objects.""" | ||||||
|  | 
 | ||||||
| from os import path | from os import path | ||||||
| from ConfigParser import ConfigParser | from ConfigParser import ConfigParser | ||||||
| import fractions | import fractions | ||||||
| @ -11,14 +13,18 @@ from elodie.localstorage import Db | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Fraction(fractions.Fraction): | class Fraction(fractions.Fraction): | ||||||
|  | 
 | ||||||
|     """Only create Fractions from floats. |     """Only create Fractions from floats. | ||||||
|  | 
 | ||||||
|  |     Should be compatible with Python 2.6, though untested. | ||||||
|  | 
 | ||||||
|     >>> Fraction(0.3) |     >>> Fraction(0.3) | ||||||
|     Fraction(3, 10) |     Fraction(3, 10) | ||||||
|     >>> Fraction(1.1) |     >>> Fraction(1.1) | ||||||
|     Fraction(11, 10) |     Fraction(11, 10) | ||||||
|     """ |     """ | ||||||
|  | 
 | ||||||
|     def __new__(cls, value, ignore=None): |     def __new__(cls, value, ignore=None): | ||||||
|         """Should be compatible with Python 2.6, though untested.""" |  | ||||||
|         return fractions.Fraction.from_float(value).limit_denominator(99999) |         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 hashlib | ||||||
| import json | import json | ||||||
| from math import radians, cos, sqrt | from math import radians, cos, sqrt | ||||||
| @ -8,6 +12,9 @@ from elodie import constants | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Db(object): | class Db(object): | ||||||
|  | 
 | ||||||
|  |     """A class for interacting with the JSON files created by Elodie.""" | ||||||
|  | 
 | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         # verify that the application directory (~/.elodie) exists, |         # verify that the application directory (~/.elodie) exists, | ||||||
|         #   else create it |         #   else create it | ||||||
| @ -47,26 +54,49 @@ class Db(object): | |||||||
|                 pass |                 pass | ||||||
| 
 | 
 | ||||||
|     def add_hash(self, key, value, write=False): |     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 |         self.hash_db[key] = value | ||||||
|         if(write is True): |         if(write is True): | ||||||
|             self.update_hash_db() |             self.update_hash_db() | ||||||
| 
 | 
 | ||||||
|     def check_hash(self, key): |     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 |         return key in self.hash_db | ||||||
| 
 | 
 | ||||||
|     def get_hash(self, key): |     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): |         if(self.check_hash(key) is True): | ||||||
|             return self.hash_db[key] |             return self.hash_db[key] | ||||||
|         return None |         return None | ||||||
| 
 | 
 | ||||||
|     def update_hash_db(self): |     def update_hash_db(self): | ||||||
|  |         """Write the hash db to disk.""" | ||||||
|         with open(constants.hash_db, 'w') as f: |         with open(constants.hash_db, 'w') as f: | ||||||
|             json.dump(self.hash_db, f) |             json.dump(self.hash_db, f) | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     http://stackoverflow.com/a/3431835/1318758 |  | ||||||
|     """ |  | ||||||
|     def checksum(self, file_path, blocksize=65536): |     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() |         hasher = hashlib.sha256() | ||||||
|         with open(file_path, 'r') as f: |         with open(file_path, 'r') as f: | ||||||
|             buf = f.read(blocksize) |             buf = f.read(blocksize) | ||||||
| @ -79,14 +109,21 @@ class Db(object): | |||||||
| 
 | 
 | ||||||
|     # Location database |     # Location database | ||||||
|     # Currently quite simple just a list of long/lat pairs with a name |     # 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: |     # structure might be needed. Some speed up ideas: | ||||||
|     # - Sort it and inter-half method can be used |     # - Sort it and inter-half method can be used | ||||||
|     # - Use integer part of long or lat as key to get a lower search list |     # - 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 |     # - Cache a small number of lookups, photos are likely to be taken in | ||||||
|     #   around a spot during import. |     #   clusters around a spot during import. | ||||||
| 
 | 
 | ||||||
|     def add_location(self, latitude, longitude, place, write=False): |     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 = {} | ||||||
|         data['lat'] = latitude |         data['lat'] = latitude | ||||||
|         data['long'] = longitude |         data['long'] = longitude | ||||||
| @ -96,10 +133,18 @@ class Db(object): | |||||||
|             self.update_location_db() |             self.update_location_db() | ||||||
| 
 | 
 | ||||||
|     def get_location_name(self, latitude, longitude, threshold_m): |     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 |         last_d = sys.maxint | ||||||
|         name = None |         name = None | ||||||
|         for data in self.location_db: |         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 |             # From http://stackoverflow.com/questions/15736995/how-can-i-quickly-estimate-the-distance-between-two-latitude-longitude-points  # noqa | ||||||
|             # convert decimal degrees to radians |             # convert decimal degrees to radians | ||||||
| 
 | 
 | ||||||
| @ -120,6 +165,11 @@ class Db(object): | |||||||
|         return name |         return name | ||||||
| 
 | 
 | ||||||
|     def get_location_coordinates(self, 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: |         for data in self.location_db: | ||||||
|             if data['name'] == name: |             if data['name'] == name: | ||||||
|                 return (data['lat'], data['long']) |                 return (data['lat'], data['long']) | ||||||
| @ -127,5 +177,6 @@ class Db(object): | |||||||
|         return None |         return None | ||||||
| 
 | 
 | ||||||
|     def update_location_db(self): |     def update_location_db(self): | ||||||
|  |         """Write the location db to disk.""" | ||||||
|         with open(constants.location_db, 'w') as f: |         with open(constants.location_db, 'w') as f: | ||||||
|             json.dump(self.location_db, f) |             json.dump(self.location_db, f) | ||||||
|  | |||||||
| @ -1,18 +1,25 @@ | |||||||
| """ | """ | ||||||
| Author: Jaisen Mathai <jaisen@jmathai.com> | The audio module contains classes specifically for dealing with audio files. | ||||||
| Audio package that handles all audio operations | The :class:`Audio` class inherits from the :class:`~elodie.media.video.Video` | ||||||
| Inherits from Video package | class. | ||||||
|  | 
 | ||||||
|  | .. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com> | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| from video import Video | from video import Video | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Audio(Video): | class Audio(Video): | ||||||
|  | 
 | ||||||
|  |     """An audio object. | ||||||
|  | 
 | ||||||
|  |     :param str source: The fully qualified path to the audio file. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|     __name__ = 'Audio' |     __name__ = 'Audio' | ||||||
|  | 
 | ||||||
|  |     #: Valid extensions for audio files. | ||||||
|     extensions = ('m4a',) |     extensions = ('m4a',) | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     @param, source, string, The fully qualified path to the audio file |  | ||||||
|     """ |  | ||||||
|     def __init__(self, source=None): |     def __init__(self, source=None): | ||||||
|         super(Audio, self).__init__(source) |         super(Audio, self).__init__(source) | ||||||
|  | |||||||
| @ -1,6 +1,12 @@ | |||||||
| """ | """ | ||||||
| Author: Jaisen Mathai <jaisen@jmathai.com> | The media module provides a base :class:`Media` class for all objects that | ||||||
| Media package that's a parent class for media objects | 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 | # load modules | ||||||
| @ -15,12 +21,14 @@ import subprocess | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Media(object): | 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' |     __name__ = 'Media' | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     @param, source, string, The fully qualified path to the video file |  | ||||||
|     """ |  | ||||||
|     def __init__(self, source=None): |     def __init__(self, source=None): | ||||||
|         self.source = source |         self.source = source | ||||||
|         self.exif_map = { |         self.exif_map = { | ||||||
| @ -33,12 +41,11 @@ class Media(object): | |||||||
|         self.exiftool_attributes = None |         self.exiftool_attributes = None | ||||||
|         self.metadata = None |         self.metadata = None | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     Get album from EXIF |  | ||||||
| 
 |  | ||||||
|     @returns, None or string |  | ||||||
|     """ |  | ||||||
|     def get_album(self): |     def get_album(self): | ||||||
|  |         """Get album from EXIF | ||||||
|  | 
 | ||||||
|  |         :returns: None or string | ||||||
|  |         """ | ||||||
|         if(not self.is_valid()): |         if(not self.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
| @ -48,29 +55,31 @@ class Media(object): | |||||||
| 
 | 
 | ||||||
|         return exiftool_attributes['album'] |         return exiftool_attributes['album'] | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     Get the full path to the video. |  | ||||||
| 
 |  | ||||||
|     @returns string |  | ||||||
|     """ |  | ||||||
|     def get_file_path(self): |     def get_file_path(self): | ||||||
|  |         """Get the full path to the video. | ||||||
|  | 
 | ||||||
|  |         :returns: string | ||||||
|  |         """ | ||||||
|         return self.source |         return self.source | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     Define is_valid to always return false. |  | ||||||
|     This should be overridden in a child class. |  | ||||||
|     """ |  | ||||||
|     def is_valid(self): |     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 |         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): |     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()): |         if(not self.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
| @ -84,6 +93,10 @@ class Media(object): | |||||||
|         return self.exif |         return self.exif | ||||||
| 
 | 
 | ||||||
|     def get_exiftool_attributes(self): |     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): |         if(self.exiftool_attributes is not None): | ||||||
|             return self.exiftool_attributes |             return self.exiftool_attributes | ||||||
| 
 | 
 | ||||||
| @ -122,25 +135,24 @@ class Media(object): | |||||||
| 
 | 
 | ||||||
|         return self.exiftool_attributes |         return self.exiftool_attributes | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     Get the file extension as a lowercased string. |  | ||||||
| 
 |  | ||||||
|     @returns, string or None for a non-video |  | ||||||
|     """ |  | ||||||
|     def get_extension(self): |     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()): |         if(not self.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
|         source = self.source |         source = self.source | ||||||
|         return os.path.splitext(source)[1][1:].lower() |         return os.path.splitext(source)[1][1:].lower() | ||||||
| 
 | 
 | ||||||
|     """ |     def get_metadata(self, update_cache=False): | ||||||
|     Get a dictionary of metadata for a photo. |         """Get a dictionary of metadata for a photo. | ||||||
|  | 
 | ||||||
|         All keys will be present and have a value of None if not obtained. |         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()): |         if(not self.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
| @ -163,12 +175,11 @@ class Media(object): | |||||||
| 
 | 
 | ||||||
|         return self.metadata |         return self.metadata | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     Get the mimetype of the file. |  | ||||||
| 
 |  | ||||||
|     @returns, string or None for a non-video |  | ||||||
|     """ |  | ||||||
|     def get_mimetype(self): |     def get_mimetype(self): | ||||||
|  |         """Get the mimetype of the file. | ||||||
|  | 
 | ||||||
|  |         :returns: str or None for a non-video | ||||||
|  |         """ | ||||||
|         if(not self.is_valid()): |         if(not self.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
| @ -179,12 +190,11 @@ class Media(object): | |||||||
| 
 | 
 | ||||||
|         return mimetype[0] |         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): |     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()): |         if(not self.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
| @ -195,14 +205,12 @@ class Media(object): | |||||||
| 
 | 
 | ||||||
|         return exiftool_attributes['title'] |         return exiftool_attributes['title'] | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     Set album for a photo |  | ||||||
| 
 |  | ||||||
|     @param, name, string, Name of album |  | ||||||
| 
 |  | ||||||
|     @returns, boolean |  | ||||||
|     """ |  | ||||||
|     def set_album(self, name): |     def set_album(self, name): | ||||||
|  |         """Set album for a photo | ||||||
|  | 
 | ||||||
|  |         :param str name: Name of album | ||||||
|  |         :returns: bool | ||||||
|  |         """ | ||||||
|         if(name is None): |         if(name is None): | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
| @ -252,27 +260,26 @@ class Media(object): | |||||||
|         self.set_album(folder) |         self.set_album(folder) | ||||||
|         return True |         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): |     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.get_metadata() | ||||||
|         self.metadata['base_name'] = new_basename |         self.metadata['base_name'] = new_basename | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     Method to manually update attributes in metadata. |  | ||||||
| 
 |  | ||||||
|     @params, named paramaters |  | ||||||
|     """ |  | ||||||
|     def set_metadata(self, **kwargs): |     def set_metadata(self, **kwargs): | ||||||
|  |         """Method to manually update attributes in metadata. | ||||||
|  | 
 | ||||||
|  |         :params dict kwargs: Named parameters to update. | ||||||
|  |         """ | ||||||
|         metadata = self.get_metadata() |         metadata = self.get_metadata() | ||||||
|         for key in kwargs: |         for key in kwargs: | ||||||
|             if(key in metadata): |             if(key in metadata): | ||||||
| @ -287,3 +294,11 @@ class Media(object): | |||||||
|                 return i(_file) |                 return i(_file) | ||||||
| 
 | 
 | ||||||
|         return None |         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> | The photo module contains the :class:`Photo` class, which is used to track | ||||||
| Photo package that handles all photo operations | image objects (JPG, DNG, etc.). | ||||||
|  | 
 | ||||||
|  | .. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com> | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| import imghdr | import imghdr | ||||||
| @ -17,25 +19,28 @@ from elodie import geolocation | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Photo(Media): | class Photo(Media): | ||||||
|  | 
 | ||||||
|  |     """A photo object. | ||||||
|  | 
 | ||||||
|  |     :param str source: The fully qualified path to the photo file | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|     __name__ = 'Photo' |     __name__ = 'Photo' | ||||||
|  | 
 | ||||||
|  |     #: Valid extensions for photo files. | ||||||
|     extensions = ('jpg', 'jpeg', 'nef', 'dng', 'gif') |     extensions = ('jpg', 'jpeg', 'nef', 'dng', 'gif') | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     @param, source, string, The fully qualified path to the photo file |  | ||||||
|     """ |  | ||||||
|     def __init__(self, source=None): |     def __init__(self, source=None): | ||||||
|         super(Photo, self).__init__(source) |         super(Photo, self).__init__(source) | ||||||
| 
 | 
 | ||||||
|         # We only want to parse EXIF once so we store it here |         # We only want to parse EXIF once so we store it here | ||||||
|         self.exif = None |         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): |     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()): |         if(not self.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
| @ -53,12 +58,13 @@ class Photo(Media): | |||||||
|                 ).group(1).replace('.', ':') |                 ).group(1).replace('.', ':') | ||||||
|         return None |         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'): |     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()): |         if(not self.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
| @ -99,13 +105,13 @@ class Photo(Media): | |||||||
|         except KeyError: |         except KeyError: | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
|     """ |     def get_date_taken(self): | ||||||
|     Get the date which the photo was taken. |         """Get the date which the photo was taken. | ||||||
|  | 
 | ||||||
|         The date value returned is defined by the min() of mtime and ctime. |         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()): |         if(not self.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
| @ -135,13 +141,14 @@ class Photo(Media): | |||||||
| 
 | 
 | ||||||
|         return time.gmtime(seconds_since_epoch) |         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): |     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 |         source = self.source | ||||||
| 
 | 
 | ||||||
|         # gh-4 This checks if the source file is an image. |         # 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 |         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): |     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): |         if(time is None): | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
| @ -172,15 +177,13 @@ class Photo(Media): | |||||||
|         exif_metadata.write() |         exif_metadata.write() | ||||||
|         return True |         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): |     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): |         if(latitude is None or longitude is None): | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
| @ -196,15 +199,12 @@ class Photo(Media): | |||||||
|         exif_metadata.write() |         exif_metadata.write() | ||||||
|         return True |         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): |     def set_title(self, title): | ||||||
|  |         """Set title for a photo. | ||||||
|  | 
 | ||||||
|  |         :param str title: Title of the photo. | ||||||
|  |         :returns: bool | ||||||
|  |         """ | ||||||
|         if(title is None): |         if(title is None): | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
| @ -216,12 +216,3 @@ class Photo(Media): | |||||||
| 
 | 
 | ||||||
|         exif_metadata.write() |         exif_metadata.write() | ||||||
|         return True |         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> | The video module contains the :class:`Video` class, which represents video | ||||||
| Video package that handles all video operations | objects (AVI, MOV, etc.). | ||||||
|  | 
 | ||||||
|  | .. moduleauthor:: Jaisen Mathai <jaisen@jmathai.com> | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| # load modules | # load modules | ||||||
| @ -21,24 +23,27 @@ from media import Media | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Video(Media): | class Video(Media): | ||||||
|  | 
 | ||||||
|  |     """A video object. | ||||||
|  | 
 | ||||||
|  |     :param str source: The fully qualified path to the video file. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|     __name__ = 'Video' |     __name__ = 'Video' | ||||||
|  | 
 | ||||||
|  |     #: Valid extensions for video files. | ||||||
|     extensions = ('avi', 'm4v', 'mov', 'mp4', '3gp') |     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): |     def __init__(self, source=None): | ||||||
|         super(Video, self).__init__(source) |         super(Video, self).__init__(source) | ||||||
| 
 | 
 | ||||||
|     """ |     def get_avmetareadwrite(self): | ||||||
|     Get path to executable avmetareadwrite binary. |         """Get path to executable avmetareadwrite binary. | ||||||
|  | 
 | ||||||
|         We wrap this since we call it in a few places and we do a fallback. |         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') |         avmetareadwrite = find_executable('avmetareadwrite') | ||||||
|         if(avmetareadwrite is None): |         if(avmetareadwrite is None): | ||||||
|             avmetareadwrite = '/usr/bin/avmetareadwrite' |             avmetareadwrite = '/usr/bin/avmetareadwrite' | ||||||
| @ -47,12 +52,11 @@ class Video(Media): | |||||||
| 
 | 
 | ||||||
|         return avmetareadwrite |         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'): |     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() |         exif_data = self.get_exif() | ||||||
|         if(exif_data is None): |         if(exif_data is None): | ||||||
|             return None |             return None | ||||||
| @ -75,13 +79,13 @@ class Video(Media): | |||||||
| 
 | 
 | ||||||
|         return decimal_degrees |         return decimal_degrees | ||||||
| 
 | 
 | ||||||
|     """ |     def get_date_taken(self): | ||||||
|     Get the date which the video was taken. |         """Get the date which the video was taken. | ||||||
|  | 
 | ||||||
|         The date value returned is defined by the min() of mtime and ctime. |         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()): |         if(not self.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
| @ -114,13 +118,13 @@ class Video(Media): | |||||||
| 
 | 
 | ||||||
|         return time.gmtime(seconds_since_epoch) |         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): |     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()): |         if(not self.is_valid()): | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
| @ -138,14 +142,14 @@ class Video(Media): | |||||||
|                 ).group(1).replace('.', ':') |                 ).group(1).replace('.', ':') | ||||||
|         return None |         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): |     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() |         exiftool = get_exiftool() | ||||||
|         if(exiftool is None): |         if(exiftool is None): | ||||||
|             return None |             return None | ||||||
| @ -158,24 +162,24 @@ class Video(Media): | |||||||
|         ) |         ) | ||||||
|         return process_output.stdout.read() |         return process_output.stdout.read() | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     Check the file extension against valid file extensions as |  | ||||||
|         returned by self.extensions |  | ||||||
| 
 |  | ||||||
|     @returns, boolean |  | ||||||
|     """ |  | ||||||
|     def is_valid(self): |     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 |         source = self.source | ||||||
|         return os.path.splitext(source)[1][1:].lower() in self.extensions |         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 |         Set the date/time a photo was taken | ||||||
| 
 | 
 | ||||||
|     @param, time, datetime, datetime object of when the photo was taken |         :param datetime date_taken_as_datetime: datetime object of when the | ||||||
| 
 |             video was recorded. | ||||||
|     @returns, boolean |         :returns: bool | ||||||
|         """ |         """ | ||||||
|     def set_date_taken(self, date_taken_as_datetime): |  | ||||||
|         if(time is None): |         if(time is None): | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
| @ -193,56 +197,51 @@ class Video(Media): | |||||||
| 
 | 
 | ||||||
|         return result |         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): |     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): |         if(latitude is None or longitude is None): | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
|         result = self.__update_using_plist(latitude=latitude, longitude=longitude)  # noqa |         result = self.__update_using_plist(latitude=latitude, longitude=longitude)  # noqa | ||||||
|         return result |         return result | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     Set title for a video |  | ||||||
| 
 |  | ||||||
|     @param, title, string, Title for the file |  | ||||||
| 
 |  | ||||||
|     @returns, boolean |  | ||||||
|     """ |  | ||||||
| 
 |  | ||||||
|     def set_title(self, title): |     def set_title(self, title): | ||||||
|  |         """Set title for a video. | ||||||
|  | 
 | ||||||
|  |         :param str title: Title for the file | ||||||
|  |         :returns: bool | ||||||
|  |         """ | ||||||
|         if(title is None): |         if(title is None): | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
|         result = self.__update_using_plist(title=title) |         result = self.__update_using_plist(title=title) | ||||||
|         return result |         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): |     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( |         if( | ||||||
|             'latitude' not in kwargs and |             'latitude' not in kwargs and | ||||||
|             'longitude' not in kwargs and |             'longitude' not in kwargs and | ||||||
| @ -400,17 +399,13 @@ class Video(Media): | |||||||
| 
 | 
 | ||||||
|             return True |             return True | ||||||
| 
 | 
 | ||||||
|     """ |  | ||||||
|     Static method to access static __valid_extensions variable. |  | ||||||
| 
 |  | ||||||
|     @returns, tuple |  | ||||||
|     """ |  | ||||||
|     @classmethod |  | ||||||
|     def get_valid_extensions(cls): |  | ||||||
|         return cls.extensions |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class Transcode(object): | 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): |     def __init__(self, video=None): | ||||||
|         self.video = video |         self.video = video | ||||||
|  | |||||||
| @ -1,8 +1,7 @@ | |||||||
| """ | """ | ||||||
| Author: Jaisen Mathai <jaisen@jmathai.com> |  | ||||||
| Parse OS X plists. | 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 | # load modules | ||||||
| @ -12,15 +11,34 @@ import plistlib | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Plist(object): | 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): |     def __init__(self, source): | ||||||
|         if not path.isfile(source): |         if not path.isfile(source): | ||||||
|             raise IOError('Could not load plist file %s' % source) |             raise IOError('Could not load plist file %s' % source) | ||||||
| 
 |  | ||||||
|         self.source = source |         self.source = source | ||||||
|         self.plist = plistlib.readPlist(self.source) |         self.plist = plistlib.readPlist(self.source) | ||||||
| 
 | 
 | ||||||
|     def update_key(self, key, value): |     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 |         self.plist[key] = value | ||||||
| 
 | 
 | ||||||
|     def write_file(self, destination): |     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) |         plistlib.writePlist(self.plist, destination) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Jaisen Mathai
						Jaisen Mathai