mirror of
https://github.com/bspeice/Melodia
synced 2024-12-26 00:28:13 -05:00
More code cleanup
Remove references to a 'collection' object - it was a half-baked idea, and the concepts it represented should be implemented in the webapp, not in the archive itself.
This commit is contained in:
parent
5433cba377
commit
190f9a7890
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
*.sqlite
|
*.sqlite
|
||||||
secret_key.py
|
secret_key.py
|
||||||
|
Melodia/cache
|
||||||
|
@ -19,4 +19,4 @@ SUPPORTED_AUDIO_EXTENSIONS = [ filetype[0] for filetype in SUPPORTED_AUDIO_FILET
|
|||||||
import django.db.models.fields
|
import django.db.models.fields
|
||||||
|
|
||||||
HASH_FUNCTION = hash
|
HASH_FUNCTION = hash
|
||||||
HASH_RESULT_DB_TYPE = django.db.models.fields.IntegerField()
|
HASH_RESULT_DB_TYPE = django.db.models.fields.IntegerField(default = -1)
|
||||||
|
@ -11,17 +11,10 @@ describes a group of songs.
|
|||||||
In this way, you back up archives of music - you don't back up the songs in a
|
In this way, you back up archives of music - you don't back up the songs in a
|
||||||
playlist. Additionally, you may want to re-organize your music to use a
|
playlist. Additionally, you may want to re-organize your music to use a
|
||||||
cleaner directory structure - a playlist doesn't care about this.
|
cleaner directory structure - a playlist doesn't care about this.
|
||||||
Note that archives are different from collections:
|
|
||||||
-Archives are physical organizations of songs. These are used in the backend.
|
|
||||||
-Collections are logical organizations of songs. These are intended to be used
|
|
||||||
on the frontend.
|
|
||||||
The difference is intended to separate the difference between logical and physical
|
|
||||||
operations. For example, you don't need to re-organize the directory structure of
|
|
||||||
a collection of songs. However, you may want to prevent kids from accessing explicit
|
|
||||||
songs even if they are part of the same archive folder as clean songs.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Archive (models.Model):
|
class Archive (models.Model):
|
||||||
|
import datetime
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The archive model itself, and all functions used to interact with it.
|
The archive model itself, and all functions used to interact with it.
|
||||||
@ -33,21 +26,21 @@ class Archive (models.Model):
|
|||||||
music files under there, and takes control of them from there.
|
music files under there, and takes control of them from there.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = models.CharField(max_length = 64)
|
name = models.CharField(max_length = 64)
|
||||||
|
|
||||||
#Note that we're not using FilePathField since this is actually a folder
|
#Note that we're not using FilePathField since this is actually a folder
|
||||||
root_folder = models.CharField(max_length = 255)
|
root_folder = models.CharField(max_length = 255)
|
||||||
|
|
||||||
#And a reference to the songs in this archive
|
#And a reference to the songs in this archive
|
||||||
songs = models.ManyToManyField(Song)
|
songs = models.ManyToManyField(Song)
|
||||||
|
|
||||||
#Backup settings
|
#Backup settings
|
||||||
backup_location = models.CharField(max_length = 255)
|
backup_location = models.CharField(max_length = 255, default = "/dev/null")
|
||||||
backup_frequency = models.IntegerField()
|
backup_frequency = models.IntegerField(default = 604800) #1 week in seconds
|
||||||
last_backup = models.DateTimeField()
|
last_backup = models.DateTimeField(default = datetime.datetime.now()) #Note that this by default will be the time the archive was instantiated
|
||||||
|
|
||||||
def _scan_filesystem(self, progress_callback = lambda x: None):
|
def _scan_filesystem(self):
|
||||||
"Scan the archive's root filesystem and add any new songs"
|
"Scan the archive's root filesystem and add any new songs without adding metadata, delete songs that exist no more"
|
||||||
#This method is implemented since the other scan methods all need to use the same code
|
#This method is implemented since the other scan methods all need to use the same code
|
||||||
#DRY FTW
|
#DRY FTW
|
||||||
import re, os
|
import re, os
|
||||||
@ -58,6 +51,7 @@ class Archive (models.Model):
|
|||||||
_regex = '|'.join(( '.*' + ext + '$' for ext in SUPPORTED_AUDIO_EXTENSIONS))
|
_regex = '|'.join(( '.*' + ext + '$' for ext in SUPPORTED_AUDIO_EXTENSIONS))
|
||||||
regex = re.compile(_regex, re.IGNORECASE)
|
regex = re.compile(_regex, re.IGNORECASE)
|
||||||
|
|
||||||
|
#Add new songs
|
||||||
for dirname, dirnames, filenames in os.walk(self.root_folder):
|
for dirname, dirnames, filenames in os.walk(self.root_folder):
|
||||||
#For each filename
|
#For each filename
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
@ -69,77 +63,29 @@ class Archive (models.Model):
|
|||||||
|
|
||||||
except ObjectDoesNotExist, e:
|
except ObjectDoesNotExist, e:
|
||||||
#Song needs to be added to database
|
#Song needs to be added to database
|
||||||
|
|
||||||
full_url = os.path.join(dirname, filename)
|
full_url = os.path.join(dirname, filename)
|
||||||
new_song = Song(url = full_url)
|
new_song = Song(url = full_url)
|
||||||
new_song.populate_metadata()
|
|
||||||
new_song.save()
|
new_song.save()
|
||||||
self.songs.add(new_song)
|
self.songs.add(new_song)
|
||||||
|
|
||||||
def quick_scan(self):
|
#Remove songs in the database if they exist no longer
|
||||||
"Scan this archive's root folder and make sure that all songs are in the database."
|
|
||||||
|
|
||||||
from os.path import isfile
|
|
||||||
|
|
||||||
#Validate existing database results
|
|
||||||
for song in self.songs.all():
|
for song in self.songs.all():
|
||||||
if not isfile(song.url):
|
if not os.path.isfile(song.url):
|
||||||
song.delete()
|
|
||||||
|
|
||||||
#Scan the root folder, and find if we need to add any new songs
|
|
||||||
self._scan_filesystem()
|
|
||||||
|
|
||||||
def scan(self):
|
|
||||||
"Scan this archive's root folder and make sure any local metadata are correct."
|
|
||||||
#Overload the regular hash function with whatever Melodia as a whole is using
|
|
||||||
from Melodia.melodia_settings import HASH_FUNCTION as hash
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
for song in self.songs.all():
|
|
||||||
|
|
||||||
if not os.path.isfile(song.song_url):
|
|
||||||
song.delete()
|
song.delete()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
#The song exists, check that the hash is the same
|
|
||||||
db_hash = song.file_hash
|
|
||||||
|
|
||||||
f = open(song_url)
|
def _update_song_metadata(self, use_echonest = False, progress_callback = lambda x, y: None):
|
||||||
file_hash = hash(f.read())
|
"Scan every song in this archive (database only) and make sure all songs are correct"
|
||||||
|
#This method operates only on the songs that are in the database - if you need to make
|
||||||
if file_hash != db_hash:
|
#sure that new songs are added, use the _scan_filesystem() method in addition
|
||||||
#Something about the song has changed, rescan the metadata
|
total_songs = self.songs.count()
|
||||||
song.populate_metadata()
|
current_song = 0
|
||||||
|
|
||||||
#Make sure to add any new songs as well
|
|
||||||
self._scan_filesystem()
|
|
||||||
|
|
||||||
|
|
||||||
def deep_scan(self):
|
|
||||||
"Scan this archive's root folder and make sure that all songs are in the database, and use EchoNest to update metadata as necessary"
|
|
||||||
|
|
||||||
#Overload the regular hash function with whatever Melodia as a whole is using
|
|
||||||
from Melodia.melodia_settings import HASH_FUNCTION as hash
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
for song in self.songs.all():
|
for song in self.songs.all():
|
||||||
|
current_song += 1
|
||||||
if not os.path.isfile(song.song_url):
|
song.populate_metadata(use_echonest = use_echonest)
|
||||||
song.delete()
|
progress_callback(current_song, total_songs)
|
||||||
continue
|
|
||||||
|
|
||||||
#The song exists, check that the hash is the same
|
|
||||||
db_hash = song.file_hash
|
|
||||||
|
|
||||||
f = open(song_url)
|
|
||||||
file_hash = hash(f.read())
|
|
||||||
|
|
||||||
if file_hash != db_hash:
|
|
||||||
#Something about the song has changed, rescan the metadata
|
|
||||||
song.populate_metadata(use_echonest = True)
|
|
||||||
|
|
||||||
#Make sure to add any new songs as well
|
|
||||||
self._scan_filesystem()
|
|
||||||
|
|
||||||
def _needs_backup(self):
|
def _needs_backup(self):
|
||||||
"Check if the current archive is due for a backup"
|
"Check if the current archive is due for a backup"
|
||||||
@ -154,6 +100,28 @@ class Archive (models.Model):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def quick_scan(self):
|
||||||
|
"Scan this archive's root folder and make sure that all songs are in the database."
|
||||||
|
#This is a quick scan - only validate whether or not songs should exist in the database
|
||||||
|
|
||||||
|
self._scan_filesystem()
|
||||||
|
|
||||||
|
def scan(self):
|
||||||
|
"Scan this archive's root folder and make sure any local metadata are correct."
|
||||||
|
#This is a longer scan - validate whether songs should exist, and use local data to update
|
||||||
|
#the database
|
||||||
|
|
||||||
|
self._scan_filesystem()
|
||||||
|
self._update_song_metadata()
|
||||||
|
|
||||||
|
def deep_scan(self):
|
||||||
|
"Scan this archive's root folder and make sure that all songs are in the database, and use EchoNest to update metadata as necessary"
|
||||||
|
#This is a very long scan - validate whether songs should exist, and use Echonest to make sure
|
||||||
|
#that metadata is as accurate as possible.
|
||||||
|
self._scan_filesystem()
|
||||||
|
self._update_song_metadata(use_echonest = True)
|
||||||
|
|
||||||
|
|
||||||
def run_backup(self, force_backup = False):
|
def run_backup(self, force_backup = False):
|
||||||
"Backup the current archive"
|
"Backup the current archive"
|
||||||
if force_backup or self._needs_backup():
|
if force_backup or self._needs_backup():
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from Melodia import melodia_settings
|
from Melodia import melodia_settings
|
||||||
|
|
||||||
|
import datetime
|
||||||
"""
|
"""
|
||||||
The Song model
|
The Song model
|
||||||
Each instance of a Song represents a single music file.
|
Each instance of a Song represents a single music file.
|
||||||
@ -8,6 +9,17 @@ This database model is used for storing the metadata information about a song,
|
|||||||
and helps in doing sorting etc.
|
and helps in doing sorting etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_default_title = "<UNAVAILABLE>"
|
||||||
|
_default_artist = "<UNAVAILABLE>"
|
||||||
|
_default_album = "<UNAVAILABLE>"
|
||||||
|
_default_release_date = datetime.datetime.now #Function will be called per new song, rather than once at loading the file
|
||||||
|
_default_genre = "<UNAVAILABLE>"
|
||||||
|
_default_bpm = -1
|
||||||
|
|
||||||
|
_default_bit_rate = -1
|
||||||
|
_default_duration = -1
|
||||||
|
_default_echonest_song_id = ""
|
||||||
|
|
||||||
class Song (models.Model):
|
class Song (models.Model):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -16,24 +28,36 @@ class Song (models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
#Standard user-populated metadata
|
#Standard user-populated metadata
|
||||||
title = models.CharField(max_length = 64)
|
title = models.CharField(max_length = 64, default = _default_title)
|
||||||
artist = models.CharField(max_length = 64)
|
artist = models.CharField(max_length = 64, default = _default_artist)
|
||||||
album = models.CharField(max_length = 64)
|
album = models.CharField(max_length = 64, default = _default_album)
|
||||||
release_date = models.DateField()
|
release_date = models.DateField(default = _default_release_date)
|
||||||
genre = models.CharField(max_length = 64)
|
genre = models.CharField(max_length = 64, default = _default_genre)
|
||||||
bpm = models.IntegerField()
|
bpm = models.IntegerField(default = _default_bpm)
|
||||||
|
|
||||||
#File metadata
|
#File metadata
|
||||||
bit_rate = models.IntegerField()
|
bit_rate = models.IntegerField(default = _default_bit_rate)
|
||||||
duration = models.IntegerField()
|
duration = models.IntegerField(default = _default_bit_rate)
|
||||||
echonest_song_id = models.CharField(max_length = 64)
|
echonest_song_id = models.CharField(max_length = 64, default = _default_echonest_song_id)
|
||||||
url = models.CharField(max_length = 64)
|
url = models.CharField(max_length = 64)
|
||||||
file_hash = melodia_settings.HASH_RESULT_DB_TYPE
|
file_hash = melodia_settings.HASH_RESULT_DB_TYPE
|
||||||
|
|
||||||
def populate_metadata(self, use_echonest = False, use_musicbrainz = False):
|
def populate_metadata(self, use_echonest = False):
|
||||||
"Populate the metadata of this song"
|
"Populate the metadata of this song (only if file hash has changed)"
|
||||||
import datetime
|
#Overload the hash function with whatever Melodia as a whole is using
|
||||||
|
from Melodia.melodia_settings import HASH_FUNCTION as hash
|
||||||
|
|
||||||
|
#Check if there's a hash entry - if there is, the song may not have changed,
|
||||||
|
#and we can go ahead and return
|
||||||
|
if self.file_hash != None:
|
||||||
|
song_file = open(self.url, 'rb')
|
||||||
|
current_file_hash = hash(song_file.read())
|
||||||
|
|
||||||
|
if current_file_hash == self.file_hash:
|
||||||
|
#The song data hasn't changed at all, we don't need to do anything
|
||||||
|
return
|
||||||
|
|
||||||
|
#If we've gotten to here, we do actually need to fully update the metadata
|
||||||
if use_echonest:
|
if use_echonest:
|
||||||
#Code to grab metadata from echonest here
|
#Code to grab metadata from echonest here
|
||||||
pass
|
pass
|
||||||
@ -49,38 +73,31 @@ class Song (models.Model):
|
|||||||
track = audiotools.open(self.url)
|
track = audiotools.open(self.url)
|
||||||
track_metadata = track.get_metadata()
|
track_metadata = track.get_metadata()
|
||||||
|
|
||||||
self.title = track_metadata.track_name or '<UNAVAILABLE>'
|
self.title = track_metadata.track_name or _default_title
|
||||||
self.artist = track_metadata.artist_name or '<UNAVAILABLE>'
|
self.artist = track_metadata.artist_name or _default_artist
|
||||||
self.album = track_metadata.album_name or '<UNAVAILABLE>'
|
self.album = track_metadata.album_name or _default_album
|
||||||
self.release_date = datetime.date(int(track_metadata.year or 1), 1, 1)
|
self.release_date = datetime.date(int(track_metadata.year or 1), 1, 1)
|
||||||
self.bpm = -1
|
self.bpm = _default_bpm
|
||||||
|
|
||||||
self.bit_rate = track.bits_per_sample() or '<UNAVAILABLE>'
|
self.bit_rate = track.bits_per_sample() or _default_bit_rate
|
||||||
self.duration = int(track.seconds_length()) or '<UNAVAILABLE>'
|
self.duration = int(track.seconds_length()) or _default_duration
|
||||||
self.echonest_song_id = ''
|
self.echonest_song_id = _default_echonest_song_id
|
||||||
|
|
||||||
except audiotools.UnsupportedFile, e:
|
except audiotools.UnsupportedFile, e:
|
||||||
#Couldn't grab the local data
|
#Couldn't grab the local data - fill in the remaining data for this record, preserving
|
||||||
#doesn't support the file, or because reading from it caused an error
|
#anything that already exists.
|
||||||
self.title = "<UNAVAILABLE>"
|
self.title = self.title or _default_title
|
||||||
self.artist = "<UNAVAILABLE>"
|
self.artist = self.artist or _default_artist
|
||||||
self.album = "<UNAVAILABLE>"
|
self.album = self.album or _default_album
|
||||||
self.release_date = datetime.datetime.now()
|
self.release_date = self.release_date or _default_release_date()
|
||||||
self.bpm = -1
|
|
||||||
|
|
||||||
self.bit_rate = -1
|
self.bpm = self.bpm or _default_bpm
|
||||||
self.duration = -1
|
self.bit_rate = self.bit_rate or _default_bitrate
|
||||||
self.echonest_song_id = ''
|
self.duration = self.bit_rate or _default_duration
|
||||||
|
self.echonest_song_id = self.echonest_song_id or _default_echonest_song_id
|
||||||
#Hash check is run regardless of what metadata method is used
|
|
||||||
if self.file_hash == None:
|
|
||||||
#Only get the hash if we really must, it's an expensive operation...
|
|
||||||
from Melodia.melodia_settings import HASH_FUNCTION as hash
|
|
||||||
f = open(self.url, 'rb')
|
|
||||||
self.file_hash = hash(f.read())
|
|
||||||
|
|
||||||
def convert(self, output_location, output_format, progress_func = lambda x, y: None):
|
def convert(self, output_location, output_format, progress_func = lambda x, y: None):
|
||||||
"Convert a song to a new format, optionally specifying what format to convert to."
|
"Convert a song to a new format."
|
||||||
#Note that output_format over-rides the format guessed by output_location
|
#Note that output_format over-rides the format guessed by output_location
|
||||||
|
|
||||||
from Melodia.resources import add_resource_dir
|
from Melodia.resources import add_resource_dir
|
||||||
|
@ -20,7 +20,7 @@ class FilesystemScanTest(TestCase):
|
|||||||
#We must save the archive before we can start adding songs to it
|
#We must save the archive before we can start adding songs to it
|
||||||
new_archive.save()
|
new_archive.save()
|
||||||
|
|
||||||
new_archive._scan_filesystem()
|
new_archive.quick_scan()
|
||||||
|
|
||||||
class ScanTest(TestCase):
|
class ScanTest(TestCase):
|
||||||
def test_archive_scan(self):
|
def test_archive_scan(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user