diff --git a/Melodia/melodia_settings.py b/Melodia/melodia_settings.py new file mode 100644 index 0000000..dd8f7ad --- /dev/null +++ b/Melodia/melodia_settings.py @@ -0,0 +1,22 @@ + +""" +This file contains settings specifically for Melodia. +The reason this was created is to control settings specifically for Melodia, +as opposed to Django at large. +""" + +#Format of this variable is a Django choices field +SUPPORTED_AUDIO_FILETYPES = ( + ('MP3', 'MPEG Layer 3'), + ('OGG', 'Ogg Vorbis Audio'), + ) + +#Use a list comprehension to grab the file extensions from our supported types +SUPPORTED_AUDIO_EXTENSIONS = [ filetype[0] for filetype in SUPPORTED_AUDIO_FILETYPES ] + +#Note that you can change this to any function you want, any +#time hashing is used by Melodia, this function is referenced +import django.db.models.fields + +HASH_FUNCTION = hash +HASH_RESULT_DB_TYPE = django.db.models.fields.IntegerField() diff --git a/Melodia/settings.py b/Melodia/settings.py index f37715b..65f6927 100644 --- a/Melodia/settings.py +++ b/Melodia/settings.py @@ -3,6 +3,10 @@ DEBUG = True TEMPLATE_DEBUG = DEBUG +# Get the project folder +import os +PROJECT_FOLDER = os.path.abspath(os.path.dirname(__file__)) + ADMINS = ( # ('Your Name', 'your_email@example.com'), ) @@ -139,6 +143,9 @@ INSTALLED_APPS = ( 'django.contrib.admin', # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', + + # Melodia apps + 'archiver', ) # A sample logging configuration. The only tangible logging @@ -169,3 +176,27 @@ LOGGING = { }, } } + +# Cache settings for all applications +# Make sure the cache folder exists first +import os +CACHE_DIR = os.path.join(PROJECT_FOLDER, "cache") + +use_cache = True +if not os.path.isdir(CACHE_DIR): + if not os.path.exists(CACHE_DIR): + #Create the cache directory + os.mkdir(CACHE_DIR) + else: + #Not a directory + import sys + sys.stderr.write('Caching disabled, cache folder is not a directory.') + use_cache = False + +if use_cache: + CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', + 'LOCATION': CACHE_DIR, + } + } diff --git a/Melodia/test_data/00 - Lecrae_Church_Clothes-front-large.jpg b/Melodia/test_data/00 - Lecrae_Church_Clothes-front-large.jpg new file mode 100644 index 0000000..bc81082 Binary files /dev/null and b/Melodia/test_data/00 - Lecrae_Church_Clothes-front-large.jpg differ diff --git a/Melodia/test_data/01 - Co-Sign (Prod by Heat Academy) (DatPiff Exclusive).mp3 b/Melodia/test_data/01 - Co-Sign (Prod by Heat Academy) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..8c81d09 Binary files /dev/null and b/Melodia/test_data/01 - Co-Sign (Prod by Heat Academy) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/02 - APB ft Thisl (Prod by Charlie Heat Sarah J) (DatPiff Exclusive).mp3 b/Melodia/test_data/02 - APB ft Thisl (Prod by Charlie Heat Sarah J) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..259293c Binary files /dev/null and b/Melodia/test_data/02 - APB ft Thisl (Prod by Charlie Heat Sarah J) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/03 - Church Clothes (Prod by Wit) (DatPiff Exclusive).mp3 b/Melodia/test_data/03 - Church Clothes (Prod by Wit) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..936f2ad Binary files /dev/null and b/Melodia/test_data/03 - Church Clothes (Prod by Wit) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/04 - Cold World ft Tasha Catour (Prod by Street Symphony) (DatPiff Exclusive).mp3 b/Melodia/test_data/04 - Cold World ft Tasha Catour (Prod by Street Symphony) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..24a4965 Binary files /dev/null and b/Melodia/test_data/04 - Cold World ft Tasha Catour (Prod by Street Symphony) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/05 - Welcome to H-Town ft Tedashii & Dre Murray (Prod By Wit) (DatPiff Exclusive).mp3 b/Melodia/test_data/05 - Welcome to H-Town ft Tedashii & Dre Murray (Prod By Wit) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..a3ac01f Binary files /dev/null and b/Melodia/test_data/05 - Welcome to H-Town ft Tedashii & Dre Murray (Prod By Wit) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/06 - Inspiration (Prod by Wit) (DatPiff Exclusive).mp3 b/Melodia/test_data/06 - Inspiration (Prod by Wit) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..3123703 Binary files /dev/null and b/Melodia/test_data/06 - Inspiration (Prod by Wit) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/07 - Rise (Prod by 9th Wonder) (DatPiff Exclusive).mp3 b/Melodia/test_data/07 - Rise (Prod by 9th Wonder) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..ed6c07d Binary files /dev/null and b/Melodia/test_data/07 - Rise (Prod by 9th Wonder) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/08 - Darkest Hour ft No Malice (Prod by ThaInnaCircle) (DatPiff Exclusive).mp3 b/Melodia/test_data/08 - Darkest Hour ft No Malice (Prod by ThaInnaCircle) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..8616f21 Binary files /dev/null and b/Melodia/test_data/08 - Darkest Hour ft No Malice (Prod by ThaInnaCircle) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/09 - Black Rose (Prod by Tyshane) (DatPiff Exclusive).mp3 b/Melodia/test_data/09 - Black Rose (Prod by Tyshane) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..24cd799 Binary files /dev/null and b/Melodia/test_data/09 - Black Rose (Prod by Tyshane) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/10 - The Price of Life ft Andy Mineo & Co Campbell (Prod by Symbolic One S1 ) (DatPiff Exclusive).mp3 b/Melodia/test_data/10 - The Price of Life ft Andy Mineo & Co Campbell (Prod by Symbolic One S1 ) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..3a48130 Binary files /dev/null and b/Melodia/test_data/10 - The Price of Life ft Andy Mineo & Co Campbell (Prod by Symbolic One S1 ) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/11 - Special ft Lester L2 Shaw (Prod by ThaInnaCircle) (DatPiff Exclusive).mp3 b/Melodia/test_data/11 - Special ft Lester L2 Shaw (Prod by ThaInnaCircle) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..4969886 Binary files /dev/null and b/Melodia/test_data/11 - Special ft Lester L2 Shaw (Prod by ThaInnaCircle) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/12 - No Regrets ft Suzy Rock (Prod by Big Juice & Street Symphony) (DatPiff Exclusive).mp3 b/Melodia/test_data/12 - No Regrets ft Suzy Rock (Prod by Big Juice & Street Symphony) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..26375ca Binary files /dev/null and b/Melodia/test_data/12 - No Regrets ft Suzy Rock (Prod by Big Juice & Street Symphony) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/13 - Gimme A Second (Prod by Boi-1da) (DatPiff Exclusive).mp3 b/Melodia/test_data/13 - Gimme A Second (Prod by Boi-1da) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..f6a7b27 Binary files /dev/null and b/Melodia/test_data/13 - Gimme A Second (Prod by Boi-1da) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/14 - Long Time Coming ft Swoope (Prod by 9th Wonder) (DatPiff Exclusive).mp3 b/Melodia/test_data/14 - Long Time Coming ft Swoope (Prod by 9th Wonder) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..5ac7ebc Binary files /dev/null and b/Melodia/test_data/14 - Long Time Coming ft Swoope (Prod by 9th Wonder) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/15 - Misconception ft Propaganda Braille Odd Thomas) (DatPiff Exclusive).mp3 b/Melodia/test_data/15 - Misconception ft Propaganda Braille Odd Thomas) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..20d9959 Binary files /dev/null and b/Melodia/test_data/15 - Misconception ft Propaganda Braille Odd Thomas) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/16 - Spazz (Prod by Charlie Heat Sarah J) (DatPiff Exclusive).mp3 b/Melodia/test_data/16 - Spazz (Prod by Charlie Heat Sarah J) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..ae7980d Binary files /dev/null and b/Melodia/test_data/16 - Spazz (Prod by Charlie Heat Sarah J) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/17 - Sacrifice (Prod by Red On The Beat Sarah J) (DatPiff Exclusive).mp3 b/Melodia/test_data/17 - Sacrifice (Prod by Red On The Beat Sarah J) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..7e69b52 Binary files /dev/null and b/Melodia/test_data/17 - Sacrifice (Prod by Red On The Beat Sarah J) (DatPiff Exclusive).mp3 differ diff --git a/Melodia/test_data/18 - Rejects ft Christon Gray (Prod by Tha Kracken) (DatPiff Exclusive).mp3 b/Melodia/test_data/18 - Rejects ft Christon Gray (Prod by Tha Kracken) (DatPiff Exclusive).mp3 new file mode 100644 index 0000000..482dffc Binary files /dev/null and b/Melodia/test_data/18 - Rejects ft Christon Gray (Prod by Tha Kracken) (DatPiff Exclusive).mp3 differ diff --git a/README.md b/README.md index 43a9cab..978b6fc 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,10 @@ Good question. Besides just wanting to learn Django, there are some good reasons Questions? Comments? Concerns? ------------------------------ Let me know! + +Development +----------- +In order to help develop for Melodia, the following packages will need to be installed (needed for audiotools, hopefully will eventually remove dependency): + +* libcdio-cdda +* libcdio-paranoia diff --git a/archiver/archive.py b/archiver/archive.py new file mode 100644 index 0000000..d92bad2 --- /dev/null +++ b/archiver/archive.py @@ -0,0 +1,118 @@ +from django.db import models + +from song import Song + +""" +This is the archive model for the archiving backend of Melodia. +It's purpose is to control the high-level functionality of managing +multiple archives of music. It is different from a playlist both conceptually +and practically - an archive describes a group of files, while a playlist +describes a group of songs. +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 +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): + + """ + The archive model itself, and all functions used to interact with it. + The archive is built up from a grouping of songs, and the functions + that are used to interact with many songs at a single time. The archive + for example allows you to re-organize a specific set of music files into + a cleaner directory structure. + The archive is given a folder to use as its root directory - it finds all + music files under there, and takes control of them from there. + """ + + name = models.CharField(max_length = 64) + + #Note that we're not using FilePathField since this is actually a folder + root_folder = models.CharField(max_length = 255) + + #And a reference to the songs in this archive + songs = models.ManyToManyField(Song) + + def _scan_filesystem(self): + "Scan the archive's root filesystem and add any new songs" + #This method is implemented since the other scan methods all need to use the same code + #DRY FTW + import re, os + from django.core.exceptions import ObjectDoesNotExist + from Melodia.melodia_settings import SUPPORTED_AUDIO_EXTENSIONS + from Melodia.melodia_settings import HASH_FUNCTION as hash + + _regex = '|'.join(( '.*' + ext + '$' for ext in SUPPORTED_AUDIO_EXTENSIONS)) + regex = re.compile(_regex, re.IGNORECASE) + + for dirname, dirnames, filenames in os.walk(self.root_folder): + #For each filename + for filename in filenames: + #If the filename is a supported audio extension + if re.match(regex, filename): + #Make sure that `filename` is in the database + try: + self.songs.get(url = filename) + + except ObjectDoesNotExist, e: + #Song needs to be added to database + print "Adding song: " + filename + + full_url = os.path.join(dirname, filename) + new_song = Song(url = full_url) + + f = open(full_url) + new_song.file_hash = hash(f.read()) + new_song.populate_metadata() + + new_song.save() + + self.songs.add(new_song) + + def scan(self): + "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(): + if not isfile(song.song_url): + song.delete() + + #Scan the root folder, and find if we need to add any new songs + self._scan_filesystem + + def deep_scan(self): + "Scan this archive's root folder and make sure that all songs are in the database, and 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(): + + if not os.path.isfile(song.song_url): + song.delete() + 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() + + #Make sure to add any new songs as well + self._scan_filesystem() + diff --git a/archiver/models.py b/archiver/models.py index 71a8362..da5acfc 100644 --- a/archiver/models.py +++ b/archiver/models.py @@ -1,3 +1,5 @@ from django.db import models # Create your models here. +from archive import Archive +from song import Song diff --git a/archiver/song.py b/archiver/song.py new file mode 100644 index 0000000..721e207 --- /dev/null +++ b/archiver/song.py @@ -0,0 +1,47 @@ +from django.db import models +from Melodia import melodia_settings + +""" +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. +""" + +class Song (models.Model): + + """ + This class defines the fields and functions related to controlling + individual music files. + """ + + #Standard user-populated metadata + title = models.CharField(max_length = 64) + artist = models.CharField(max_length = 64) + album = models.CharField(max_length = 64) + release_date = models.DateField() + genre = models.CharField(max_length = 64) + bpm = models.IntegerField() + + + #File metadata + bit_rate = models.IntegerField() + duration = models.IntegerField() + echonest_song_id = models.CharField(max_length = 64) + url = models.CharField(max_length = 64) + file_hash = melodia_settings.HASH_RESULT_DB_TYPE + + def populate_metadata(self): + "Populate the metadata of this song" + #Will eventually use EchoNest to power this. For now, just use defaults. + import datetime + self.title = "_" + self.artist = "_" + self.album = "_" + self.release_date = datetime.datetime.now() + self.genre = "_" + self.bpm = 0 + + self.bit_rate = 0 + self.duration = 0 + self.echonest_song_id = "_" diff --git a/archiver/tests.py b/archiver/tests.py index 501deb7..70e110d 100644 --- a/archiver/tests.py +++ b/archiver/tests.py @@ -7,10 +7,47 @@ Replace this with more appropriate tests for your application. from django.test import TestCase +class FilesystemScanTest(TestCase): + def test_filesystem_scan(self): + "Tests that we can scan a filesystem correctly." + import os + from archiver.archive import Archive + from Melodia.settings import PROJECT_FOLDER -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) + TEST_DATA_FOLDER = os.path.join(PROJECT_FOLDER, "test_data") + new_archive = Archive(root_folder = TEST_DATA_FOLDER) + + #We must save the archive before we can start adding songs to it + new_archive.save() + + new_archive._scan_filesystem() + +class ScanTest(TestCase): + def test_archive_scan(self): + "Tests that we can scan an archive correctly." + import os + from archiver.archive import Archive + from Melodia.settings import PROJECT_FOLDER + + TEST_DATA_FOLDER = os.path.join(PROJECT_FOLDER, "test_data") + new_archive = Archive(root_folder = TEST_DATA_FOLDER) + + #We must save the archive before we can start adding songs to it + new_archive.save() + + new_archive.scan() + +class DeepScanTest(TestCase): + def test_archive_deep_scan(self): + "Tests that we can deep scan an archive correctly." + import os + from archiver.archive import Archive + from Melodia.settings import PROJECT_FOLDER + + TEST_DATA_FOLDER = os.path.join(PROJECT_FOLDER, "test_data") + new_archive = Archive(root_folder = TEST_DATA_FOLDER) + + #We must save the archive before we can start adding songs to it + new_archive.save() + + new_archive.deep_scan()