Melodia/archiver/models/playlist.py

230 lines
6.6 KiB
Python

"""
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 archiver.listfield import IntegerListField
import re
from warnings import warn
class Playlist(models.Model):
class Meta:
app_label = 'archiver'
"""
.. 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()
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.song_list.insert(position, new_song.id)
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.song_list.append(new_song.id)
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]
if new_position < original_position:
del self.song_list[original_position]
self.song_list.insert(new_position, song_id)
else:
del self.song_list[original_position]
self.song_list.insert(new_position - 1, song_id) #Account for the list indices shifting down.
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]
def export(self, playlist_type = "m3u"):
"""
Export this internal playlist to a file format.
Supported formats:
* 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]"
#Playlist body
for index, song_id in enumerate(self.song_list):
song = self.songs.get(id = song_id)
playlist_string += "File" + str(index + 1) + "=" + song.url + "\n"
playlist_string += "Title" + str(index + 1) + "=" + song.title + "\n"
playlist_string += "Length" + str(index + 1) + "=" + str(song.duration) + "\n"
#Playlist footer
playlist_string += "NumberOfEntries=" + str(len(self.song_list))
playlist_string += "Version=2"
return playlist_string
elif playlist_type == "m3u":
#Playlist header
playlist_string += "#EXTM3U" + "\n"
#Playlist body
for song_id in self.song_list:
song = self.songs.get(id = song_id)
playlist_string += "#EXTINF:" + str(song.duration) + "," + song.artist + " - " + song.title + "\n"
playlist_string += song.url + "\n\n"
return playlist_string
def playlist_import(self, playlist_string = None):
"""
Import and convert a playlist into native DB format.
: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 = []
if not playlist_string:
#Make sure we have a string to operate on.
return False
#Figure out what format we're in
if playlist_string[0:7] == "#EXTM3U":
#Import m3u format playlist
print playlist_string
#Expected format is "#EXTINF:" followed by the song url on the next line.
line_iterator = playlist_string.split("\n").__iter__()
#In case we end iteration early
try:
for line in line_iterator:
if line[0:8] == "#EXTINF:":
song_url = line_iterator.next() #Consume the next line
try:
song = Song.objects.get(url = song_url)
self.append(song)
except ObjectDoesNotExist:
#The URL of our song could not be found
warn("The playlist entry: " + song_url + " could not be found, and has not been added to your playlist.")
continue
#Silently end processing
except StopIteration:
pass
return True
if playlist_string[0:10] == "[playlist]":
#Import pls format playlist
#This one is a bit simpler - we're just looking for lines that start with "File="
pls_regex = re.compile("^File=", re.IGNORECASE)
for file_line in pls_regex.match(pls_regex, playlist_string):
song_url = file_line[5:]
try:
song = Song.objects.get(url = song_url)
self.append(song)
except ObjectDoesNotExist:
#The URL of our song could not be found
warn("The playlist entry: " + song_url + " could not be found, and has not been added to your playlist.")
continue
return True
#If we got here, the playlist format wasn't recognized.
return False