mirror of
				https://github.com/bspeice/Melodia
				synced 2025-11-04 02:10:42 -05:00 
			
		
		
		
	Upload the initial documentation for the archiver application
This commit is contained in:
		@ -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
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user