mirror of
https://github.com/bspeice/Melodia
synced 2024-12-26 00:28:13 -05:00
Commit initial archive code and test data
This commit is contained in:
parent
5004f5f590
commit
d31961e029
22
Melodia/melodia_settings.py
Normal file
22
Melodia/melodia_settings.py
Normal file
@ -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()
|
@ -3,6 +3,10 @@
|
|||||||
DEBUG = True
|
DEBUG = True
|
||||||
TEMPLATE_DEBUG = DEBUG
|
TEMPLATE_DEBUG = DEBUG
|
||||||
|
|
||||||
|
# Get the project folder
|
||||||
|
import os
|
||||||
|
PROJECT_FOLDER = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
ADMINS = (
|
ADMINS = (
|
||||||
# ('Your Name', 'your_email@example.com'),
|
# ('Your Name', 'your_email@example.com'),
|
||||||
)
|
)
|
||||||
@ -139,6 +143,9 @@ INSTALLED_APPS = (
|
|||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
# Uncomment the next line to enable admin documentation:
|
# Uncomment the next line to enable admin documentation:
|
||||||
# 'django.contrib.admindocs',
|
# 'django.contrib.admindocs',
|
||||||
|
|
||||||
|
# Melodia apps
|
||||||
|
'archiver',
|
||||||
)
|
)
|
||||||
|
|
||||||
# A sample logging configuration. The only tangible logging
|
# 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BIN
Melodia/test_data/00 - Lecrae_Church_Clothes-front-large.jpg
Normal file
BIN
Melodia/test_data/00 - Lecrae_Church_Clothes-front-large.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 167 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -39,3 +39,10 @@ Good question. Besides just wanting to learn Django, there are some good reasons
|
|||||||
Questions? Comments? Concerns?
|
Questions? Comments? Concerns?
|
||||||
------------------------------
|
------------------------------
|
||||||
Let me know!
|
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
|
||||||
|
118
archiver/archive.py
Normal file
118
archiver/archive.py
Normal file
@ -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()
|
||||||
|
|
@ -1,3 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
from archive import Archive
|
||||||
|
from song import Song
|
||||||
|
47
archiver/song.py
Normal file
47
archiver/song.py
Normal file
@ -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 = "_"
|
@ -7,10 +7,47 @@ Replace this with more appropriate tests for your application.
|
|||||||
|
|
||||||
from django.test import TestCase
|
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):
|
TEST_DATA_FOLDER = os.path.join(PROJECT_FOLDER, "test_data")
|
||||||
def test_basic_addition(self):
|
new_archive = Archive(root_folder = TEST_DATA_FOLDER)
|
||||||
"""
|
|
||||||
Tests that 1 + 1 always equals 2.
|
#We must save the archive before we can start adding songs to it
|
||||||
"""
|
new_archive.save()
|
||||||
self.assertEqual(1 + 1, 2)
|
|
||||||
|
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()
|
||||||
|
Loading…
Reference in New Issue
Block a user