Upload the initial documentation for the archiver application

master
Bradlee Speice 2013-05-09 23:30:35 -04:00
parent a515a4b8d3
commit 441f57bb2b
10 changed files with 354 additions and 104 deletions

56
archiver/listfield.py Normal file
View File

@ -0,0 +1,56 @@
'''
Testing documentation
'''
from django.db import models
import re, itertools
class IntegerListField(models.TextField):
"""
Store a list of integers in a database string.
Format is:
[<int_1>, <int_2>, <int_3>, ... , <int_n>]
"""
description = "Field type for storing lists of integers."
__metaclass__ = models.SubfieldBase
def __init__(self, *args, **kwargs):
super(IntegerListField, self).__init__(*args, **kwargs)
#Convert database to python
def to_python(self, value):
if isinstance(value, list):
return value
#Process a database string
#Validation first
if len(value) <= 0:
return []
if value[0] != '[' or value[-1] != ']':
raise ValidationError("Invalid input to parse a list of integers!")
#Note that any non-digit string is a valid separator
_csv_regex = "[0-9]"
csv_regex = re.compile(_csv_regex)
#Synonymous to:
#string_list = filter(None, csv_regex.findall(value))
string_list = itertools.ifilter(None, csv_regex.findall(value))
value_list = [int(i) for i in string_list]
return value_list
#Convert python to database
def get_prep_value(self, value):
if not isinstance(value, list):
raise ValidationError("Invalid list given to put in database!")
separator_string = ", "
list_elements = separator_string.join(map(str, value))
return "[" + list_elements + "]"

View File

@ -1,10 +1,3 @@
'''
.. currentmodule:: archiver.models
I'm trying to link to :class:`~archiver.models.archive.Archive`!
'''
# Create your models here.
from archive import Archive
from song import Song

View File

@ -1,6 +1,4 @@
"""
.. module:: archiver.models.archive
This is the Archive model for the backend of Melodia. It's functionality is to
provide a grouping of songs based on where they are located in the filesystem.
It controls the high-level functionality of managing multiple archives

View File

@ -113,5 +113,8 @@ class Feed(models.Model):
:param dry_run: Calculate what would have been downloaded or deleted, but do not actually do either.
:param forbid_delete: Run, and only download new episodes. Ignores the :data:`max_episodes` field for this podcast.
.. todo::
Actually write this method...
"""
pass

View File

@ -1,24 +1,17 @@
"""
.. module:: archiver.models
Playlist model
Each playlist is a high-level ordering of songs. There really isn't much to a playlist - just its name, and the songs inside it.
However, we need to have a way to guarantee song order, in addition to re-ordering. A ManyToMany field can't do this.
As such, a custom IntegerListField is implemented - it takes a python list of ints, converts it to a text field in the DB,
and then back to a python list. This way, we can guarantee order, and have a song appear multiple times.
The IntegerListField itself uses the ID of each song as the int in a list. For example, a list of:
[1, 3, 5, 17]
Means that the playlist is made up of four songs. The order of the playlist is the song with index 1, 3, 5, and 17.
Additionally, the ManyToMany field is included to make sure we don't use the global Songs manager - it just seems hackish.
The Playlist model is simply that - it's a playlist of songs. However, we do
have to guarantee the song order, in addition to re-ordering the playlist.
As such, a :class:`models.ManyToManyField` isn't sufficient. We use a custom
database field to store a list of integers - the :class:`IntegerListField`.
This way we can guarantee song order, re-order the playlist, have songs
appear multiple times, etc.
"""
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from song import Song
from listfield import IntegerListField
from archiver.listfield import IntegerListField
import re
from warnings import warn
@ -28,83 +21,74 @@ class Playlist(models.Model):
app_label = 'archiver'
"""
The Playlist class defines the playlist model and its operations.
Currently supported are add, move, and remove operations, as well as exporting to
multiple formats.
.. data:: name
String with the human-readable name for this playlist.
.. data:: song_list
List made up of Python integers. Each integer is assumed
to be a primary key to the :data:`Song.id` field for a song.
"""
name = models.CharField(max_length = 255)
song_list = IntegerListField()
#This is a bit of a backup field, since technically the song PK's are in
#the song_order field. However, it might be useful to just get a reference
#to the songs in this playlist. Additionally, it's kind of hackish to reference
#the global Song manager, rather than something that is specific for this playlist.
songs = models.ManyToManyField(Song)
def _populate_songs(self):
"""
Make sure that the 'songs' relation is up-to-date.
"""
#This operation works by getting the ID's for all songs currently in the playlist,
#calculates what we need to add, what we need to remove, and then does it.
#As much work as is possible is done on the python side to avoid the DB at all costs.
current_song_ids = [song.id for song in self.songs.all()]
current_song_ids_set = set(current_song_ids)
new_song_ids_set = set(self.song_list)
remove_set = current_song_ids_set.difference(new_song_ids_set)
add_set = new_song_ids_set.difference(current_song_ids_set)
for song_id in remove_set:
song = self.songs.get(id = song_id)
song.remove()
for song_id in add_set:
song = Song.objects.get(id = song_id) #Using the global Songs manager is unavoidable for this one
self.songs.add(song)
def insert(self, position, new_song):
"""
Insert a new song into the playlist at a specific position.
:param position: **Index** for the position this new song should be inserted at.
:param new_song: Reference to a :class:`Song` instance that will be inserted.
"""
if not isinstance(new_song, Song):
#Not given a song reference, raise an error
raise ValidationError("Not given a song reference to insert.")
self.songs.add(new_song)
self.song_list.insert(position, new_song.id)
self._populate_songs()
def append(self, new_song):
"""
Add a new song to the end of the playlist.
:param new_song: Reference to a :class:`Song` instance to be appended.
"""
if not isinstance(new_song, Song):
#Not given a song reference, raise an error
raise ValidationError("Not given a song reference to insert.")
self.songs.add(new_song)
self.song_list.append(new_song.id)
self._populate_songs()
def move(self, original_position, new_position):
"""
Move a song from one position to another
:param original_position: The index of the song we want to move
:param new_position: The index of where the song should be. See note below.
.. note::
When moving songs, it's a bit weird since the index we're actually
moving to may change. Consider the scenario --
* Function called with indexes 4 and 6
* Song removed from index 4
* The song that was in index 6 is now at index 5
* Song inserted at index 6 in new list - one further than originally intended.
As such, the behavior is that the song at index ``original_position`` is placed
above the song at ``new_position`` when this function is called.
"""
if original_position == new_position:
return
song_id = self.song_list[original_position]
#This is actually a bit more complicated than it first appears, since the index we're moving to may actually change
#when we remove an item.
if new_position < original_position:
del self.song_list[original_position]
self.song_list.insert(new_position, song_id)
@ -113,25 +97,29 @@ class Playlist(models.Model):
del self.song_list[original_position]
self.song_list.insert(new_position - 1, song_id) #Account for the list indices shifting down.
self._populate_songs()
def remove(self, position):
"""
Remove a song from this playlist.
:param position: Index of the song to be removed
"""
if position > len(self.song_list):
return False
del self.song_list[position]
self._populate_songs()
def export(self, playlist_type = "m3u"):
"""
Export this internal playlist to a file format.
Supported formats:
-pls
-m3u
Return value is a string containing the playlist -
you can write it to file as you deem necessary.
* pls
* m3u
:param playlist_type: String containing the file type to export to
:rtype: String containing the file content for this playlist.
"""
if playlist_type == "pls":
#Playlist header
playlist_string = "[playlist]"
@ -162,13 +150,26 @@ class Playlist(models.Model):
return playlist_string
def _import(self, playlist_string = None):
def playlist_import(self, playlist_string = None):
"""
Import and convert a playlist into native DB format.
This function will return true if the playlist format was recognized, false otherwise.
It will return true even if there are errors processing individual songs in the playlist.
As a side note - the _import() name is used since python doesn't let
you name a function import().
:param playlist_string: A string with the file content we're trying to import.
:rtype: Returns true of the playlist format was recognized. See notes on processing below.
.. warning::
The semantics on returning are nitpicky. This function will return ``False`` if the
playlist format was not recognized. If there are errors in processing, this
function will still return ``True``.
For example, if you try to import a song which does not exist in an :class:`Archive`,
it will fail that song silently.
.. todo::
Actually write the import code.
"""
#TODO: Code playlist importing
self.song_list = []

View File

@ -1,3 +1,9 @@
"""
The :class:`Song` model is by far the most complicated and involved model.
Each instance is a single music file. This model is used to store metadata
about the song.
"""
from django.db import models
from Melodia import melodia_settings
@ -5,12 +11,6 @@ from archive import Archive
import datetime
import os.path
"""
The Song model
Each instance of a Song represents a single music file.
This database model is used for storing the metadata information about a song,
and helps in doing sorting etc.
"""
_default_string = "_UNAVAILABLE_"
_default_date = datetime.datetime.now
@ -32,18 +32,119 @@ _default_rating_choices = (
)
class Song (models.Model):
class Meta:
app_label = 'archiver'
"""
This class defines the fields and functions related to controlling
individual music files.
Note that the Playlist model depends on this model's PK being an int.
.. data:: title
Title tag of this song
.. data:: artist
Artist tag of this song.
.. data:: album_artist
Album artist tag of this song. Can be used to group albums where
individual songs were made by different people.
.. data:: album
Album tag of this song.
.. data:: year
Integer representing the year this song was made.
.. data:: genre
Genre tag of this song. This is a general :class:`models.CharField`
field, and is not limited to a specific set of genres.
.. data:: bpm
Beats per minute of this song (integer).
.. data:: disc_number
Disc number this song came from
.. data:: disc_total
Total number of discs in the album this song is from
.. data:: track_number
Track number in the album this song is from
.. data:: track_total
Total number of tracks in the album this song is from
.. data:: comment
Comment tag of this song
.. data:: bit_rate
Integer representing the bit rate of this song
.. data:: duration
Duration (in seconds, floating-point value) of this song
.. data:: add_date
Date (not time) this song was added to the DB. Should **not** be
modified outside of this class' methods.
.. data:: url
URL for where this file is located on disk.
.. data:: file_hash
The hash string for this file - used to quickly check if the file has
been modified.
.. data:: file_size
Size of the file in bytes.
.. data:: play_count
How many times this file has been played through (defined as greater
than 50% of the song heard before skipping)
.. data:: skip_count
How many times this file has been skipped (defined as less than 50% of
the song heard before skipping)
.. data:: rating
Rating for this song. Ratings are as follows in order of increasing favoredness
on a 1--5 scale --
========= ======== ================
Rating: Value: Class field:
========= ======== ================
Default 0 RATING_DEFAULT
Bad 1 RATING_BAD
OK 2 RATING_OK
Decent 3 RATING_DECENT
Good 4 RATING_GOOD
Excellent 5 RATING_EXCELLENT
========= ======== ================
.. todo::
Change defaults to allow for ``None`` instead
Change private functions to public as need be
"""
#Standard song metadata
title = models.CharField(max_length = 64, default = _default_string)
artist = models.CharField(max_length = 64, default = _default_string)
album_artist = models.CharField(max_length = 64, default = _default_string)
album = models.CharField(max_length = 64, default = _default_string)
year = models.IntegerField(default = _default_string)
genre = models.CharField(max_length = 64, default = _default_string)
@ -58,7 +159,6 @@ class Song (models.Model):
bit_rate = models.IntegerField(default = _default_int)
duration = models.FloatField(default = _default_int)
add_date = models.DateField(default = _default_date)
echonest_song_id = models.CharField(max_length = 64, default = _default_string)
url = models.CharField(max_length = 255)
file_hash = melodia_settings.HASH_RESULT_DB_TYPE
file_size = models.IntegerField(default = _default_int)
@ -79,6 +179,9 @@ class Song (models.Model):
RATING_GOOD = _default_rating_good
RATING_EXCELLENT = _default_rating_excellent
class Meta:
app_label = 'archiver'
def _get_full_url(self):
"Combine this song's URL with the URL of its parent"
return os.path.join(parent_archive.root_folder, self.url)
@ -111,10 +214,6 @@ class Song (models.Model):
self.file_hash = hash(file_handle.read())
self.file_size = os.stat(self._get_full_url).st_size
def _grab_metadata_echonest(self):
"Populate this song's metadata using EchoNest"
pass
def _grab_metadata_local(self):
"Populate this song's metadata using what is locally available"
#Use mutagen to get the song metadata
@ -145,8 +244,10 @@ class Song (models.Model):
#Couldn't grab the local data
return False
def populate_metadata(self, use_echonest = False):
"Populate the metadata of this song (only if file hash has changed), and save the result."
def populate_metadata(self):
"""
Populate the metadata of this song (only if file hash has changed), and save the result.
"""
if self._file_not_changed():
return
@ -157,6 +258,15 @@ class Song (models.Model):
else:
self._grab_metadata_local()
def convert(self, output_location, output_format, progress_func = lambda x, y: None):
"Convert a song to a new format."
def convert(self, output_location, output_format):
"""
Convert a song to a new format.
:param output_location: String URL of where the resulting file should be stored
:param output_format: Output format of the resulting file
.. todo::
Actually write the code to convert files, or abandon if necessary
"""
pass #Need to get pydub code in place

View File

@ -26,7 +26,13 @@ sys.path.insert(0, os.path.abspath('../..')) # Django project root
# 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', 'sphinx.ext.coverage', 'sphinx.ext.viewcode']
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.todo']
# Enable TODO support
todo_include_todos = True
# Document class members in source order
autodoc_member_order = 'bysource'
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

View File

@ -0,0 +1,39 @@
models Package
==============
:mod:`models` Package
---------------------
.. automodule:: archiver.models
:members:
:undoc-members:
:show-inheritance:
:mod:`archive` Module
---------------------
.. automodule:: archiver.models.archive
:members:
:show-inheritance:
:mod:`feed` Module
------------------
.. automodule:: archiver.models.feed
:members:
:show-inheritance:
:mod:`playlist` Module
----------------------
.. automodule:: archiver.models.playlist
:members:
:show-inheritance:
:mod:`song` Module
------------------
.. automodule:: archiver.models.song
:members:
:show-inheritance:

View File

@ -1,5 +1,42 @@
====
Archive backend documentation
====
archiver Package
================
:mod:`archiver` Package
-----------------------
.. automodule:: archiver.__init__
:members:
:undoc-members:
:show-inheritance:
:mod:`listfield` Module
-----------------------
.. automodule:: archiver.listfield
:members:
:undoc-members:
:show-inheritance:
:mod:`tests` Module
-------------------
.. automodule:: archiver.tests
:members:
:undoc-members:
:show-inheritance:
:mod:`views` Module
-------------------
.. automodule:: archiver.views
:members:
:undoc-members:
:show-inheritance:
Subpackages
-----------
.. toctree::
archiver.models
.. automodule:: archiver

7
doc/docs/modules.rst Normal file
View File

@ -0,0 +1,7 @@
archiver
========
.. toctree::
:maxdepth: 4
archiver