mirror of
https://github.com/bspeice/Melodia
synced 2024-12-04 13:58:18 -05:00
Upload the initial documentation for the archiver application
This commit is contained in:
parent
a515a4b8d3
commit
441f57bb2b
56
archiver/listfield.py
Normal file
56
archiver/listfield.py
Normal 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 + "]"
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 = []
|
||||
|
@ -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
|
||||
|
@ -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']
|
||||
|
39
doc/docs/archiver.models.rst
Normal file
39
doc/docs/archiver.models.rst
Normal 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:
|
||||
|
@ -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
7
doc/docs/modules.rst
Normal file
@ -0,0 +1,7 @@
|
||||
archiver
|
||||
========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
archiver
|
Loading…
Reference in New Issue
Block a user