mirror of
				https://github.com/bspeice/Melodia
				synced 2025-11-04 02:10:42 -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:
		
							
								
								
									
										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)
 | 
					 | 
				
			||||||
			file_hash = hash(f.read())
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if file_hash != db_hash:
 | 
						def _update_song_metadata(self, use_echonest = False, progress_callback = lambda x, y: None):
 | 
				
			||||||
				#Something about the song has changed, rescan the metadata
 | 
							"Scan every song in this archive (database only) and make sure all songs are correct"
 | 
				
			||||||
				song.populate_metadata()
 | 
							#This method operates only on the songs that are in the database - if you need to make
 | 
				
			||||||
 | 
							#sure that new songs are added, use the _scan_filesystem() method in addition
 | 
				
			||||||
		#Make sure to add any new songs as well
 | 
							total_songs  = self.songs.count()
 | 
				
			||||||
		self._scan_filesystem()
 | 
							current_song = 0
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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):
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user