230 lines
6.6 KiB
Python
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
|