minor bug fixes. known stutter bug in database/meta systems appears to be the delay in connecting to the database. future work: 1. intergrate firefly.dll as server module. 2. ensure full database funcctionality. 3. add toggle to use database or not. (by default we check for firefly if we don't find we default to python systems. if found we automatically use firefly.(so maybe on toggles)) 4. investigate and fix the stutter bug.
289 lines
11 KiB
Python
289 lines
11 KiB
Python
import logging
|
|
from datetime import datetime
|
|
from ifireflylib import IFireflyClient as FireflyDatabase
|
|
|
|
# Get the logger
|
|
logger = logging.getLogger("fbroswer")
|
|
|
|
|
|
class FireflyDB:
|
|
def __init__(self):
|
|
# Initialize with default values
|
|
self.use_db = False
|
|
self.db = None
|
|
self.artists = set()
|
|
self.albums = set()
|
|
self.genres = set()
|
|
self.years = set()
|
|
logger.debug("FireflyDB instance initialized")
|
|
|
|
def connect_to(
|
|
self, use_db=False, db_host="localhost", db_port=6379, db_password=None
|
|
):
|
|
# Set the use_db flag
|
|
self.use_db = use_db
|
|
logger.info(f"Database usage set to: {use_db}")
|
|
|
|
# Metadata organization
|
|
self.artists = set()
|
|
self.albums = set()
|
|
self.genres = set()
|
|
self.years = set()
|
|
|
|
if use_db:
|
|
try:
|
|
logger.info(f"Connecting to FireflyDB at {db_host}:{db_port}")
|
|
self.db = FireflyDatabase(
|
|
host=db_host, port=db_port, password=db_password
|
|
)
|
|
# Test connection
|
|
if not self.db.ping():
|
|
logger.warning(
|
|
"Could not connect to FireflyDB. Continuing without database."
|
|
)
|
|
self.use_db = False
|
|
self.db = None
|
|
else:
|
|
logger.info("Successfully connected to FireflyDB")
|
|
# Store connection timestamp
|
|
timestamp = str(datetime.now())
|
|
logger.debug(
|
|
f"Setting last_connection timestamp: {timestamp}"
|
|
)
|
|
self.db.string_ops.string_set("last_connection", timestamp)
|
|
logger.debug("Connection timestamp stored successfully")
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error connecting to FireflyDB: {e}", exc_info=True
|
|
)
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
self.use_db = False
|
|
self.db = None
|
|
|
|
def close(self):
|
|
"""Close database connection when done"""
|
|
if self.db:
|
|
logger.info("Closing database connection")
|
|
self.db.close()
|
|
self.db = None
|
|
|
|
def has_metadata_in_db(self, file_path):
|
|
"""Check if metadata for a file already exists in the database"""
|
|
if not self.use_db or not self.db:
|
|
return False
|
|
|
|
try:
|
|
key = f"audio:{file_path}"
|
|
# Use hash_field_exists to check if the key exists in the database
|
|
exists = self.db.hash_ops.hash_field_exists(key, "title")
|
|
logger.debug(
|
|
f"Metadata check for {file_path}: {'exists' if exists else 'not found'}"
|
|
)
|
|
return exists
|
|
except Exception as e:
|
|
logger.error(f"Error checking metadata in database: {e}")
|
|
return False
|
|
|
|
def store_metadata(self, metadata):
|
|
"""Store audio file metadata in the database"""
|
|
if not self.use_db or not self.db:
|
|
logger.info("Database usage is disabled, not storing metadata")
|
|
return False
|
|
|
|
try:
|
|
# Use the file path as a unique key
|
|
file_path = metadata["file_path"]
|
|
key = f"audio:{file_path}"
|
|
|
|
# Log the storage attempt
|
|
logger.info(f"Storing metadata for {file_path} in FireflyDB")
|
|
logger.debug(f"Metadata fields: {list(metadata.keys())}")
|
|
|
|
# Store as a hash with all metadata fields
|
|
success = True
|
|
for field, value in metadata.items():
|
|
if field != "file_path": # Skip using file_path as a field
|
|
logger.debug(f" Setting field {field}={value}")
|
|
# Use the direct hash_set method instead of hash_ops
|
|
result = self.db.hash_ops.hash_set(key, field, value)
|
|
if not result:
|
|
logger.warning(
|
|
f" Failed to store field {field} for {file_path}"
|
|
)
|
|
success = False
|
|
else:
|
|
logger.debug(f" Successfully stored field {field}")
|
|
|
|
# Add to index lists for quick lookup
|
|
if "artist" in metadata and metadata["artist"]:
|
|
artist_key = f"index:artist:{metadata['artist']}"
|
|
logger.debug(f" Adding to artist index: {artist_key}")
|
|
self.db.list_ops.list_right_push(artist_key, file_path)
|
|
|
|
if "album" in metadata and metadata["album"]:
|
|
album_key = f"index:album:{metadata['album']}"
|
|
logger.debug(f" Adding to album index: {album_key}")
|
|
self.db.list_ops.list_right_push(album_key, file_path)
|
|
|
|
if "genre" in metadata and metadata["genre"]:
|
|
genre_key = f"index:genre:{metadata['genre']}"
|
|
logger.debug(f" Adding to genre index: {genre_key}")
|
|
self.db.list_ops.list_right_push(genre_key, file_path)
|
|
|
|
# Add to a master list of all audio files for easy retrieval
|
|
logger.debug(" Adding to master audio files list")
|
|
self.db.list_ops.list_right_push("all_audio_files", file_path)
|
|
|
|
# Store timestamp of when metadata was added
|
|
self.db.hash_ops.hash_set(key, "timestamp", str(datetime.now()))
|
|
|
|
# Verify storage by retrieving one field
|
|
if "title" in metadata:
|
|
retrieved_title = self.db.hash_ops.hash_get(key, "title")
|
|
logger.debug(
|
|
f" Verification - Retrieved title: {retrieved_title}"
|
|
)
|
|
|
|
# Strip quotes if present in the retrieved value
|
|
if retrieved_title and isinstance(retrieved_title, str):
|
|
if retrieved_title.startswith(
|
|
'"'
|
|
) and retrieved_title.endswith('"'):
|
|
retrieved_title = retrieved_title[1:-1]
|
|
|
|
# Compare the cleaned value
|
|
if retrieved_title != metadata["title"]:
|
|
logger.warning(
|
|
f" Verification failed: expected '{metadata['title']}', got '{retrieved_title}'"
|
|
)
|
|
# Don't mark as failure if it's just a quoting issue
|
|
if retrieved_title.strip('"') == metadata["title"]:
|
|
logger.info(
|
|
" Verification passed after stripping quotes"
|
|
)
|
|
else:
|
|
success = False
|
|
|
|
logger.info(
|
|
f"Metadata storage {'successful' if success else 'partially failed'} for {file_path}"
|
|
)
|
|
return success
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Error storing metadata in database: {e}", exc_info=True
|
|
)
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
return False
|
|
|
|
def get_metadata_from_db(self, file_path):
|
|
"""Retrieve metadata for a file from the database"""
|
|
if not self.use_db or not self.db:
|
|
return None
|
|
|
|
try:
|
|
key = f"audio:{file_path}"
|
|
metadata = self.db.hash_ops.hash_get_all(key)
|
|
|
|
if metadata:
|
|
# Add the file path to the metadata
|
|
metadata["file_path"] = file_path
|
|
logger.debug(
|
|
f"Retrieved metadata for {file_path} from database"
|
|
)
|
|
return metadata
|
|
logger.debug(f"No metadata found for {file_path} in database")
|
|
return None
|
|
except Exception as e:
|
|
logger.error(f"Error retrieving metadata from database: {e}")
|
|
return None
|
|
|
|
def search_by_artist(self, artist):
|
|
"""Search for files by artist"""
|
|
if not self.use_db or not self.db:
|
|
return []
|
|
|
|
try:
|
|
key = f"index:artist:{artist}"
|
|
logger.debug(f"Searching for files by artist: {artist}")
|
|
return self.db.list_ops.list_range(key, 0, -1)
|
|
except Exception as e:
|
|
logger.error(f"Error searching by artist: {e}")
|
|
return []
|
|
|
|
# Similar methods for album and genre searches
|
|
|
|
def process_metadata(self, metadata):
|
|
"""Process extracted metadata"""
|
|
if "artist" in metadata and metadata["artist"]:
|
|
self.artists.add(metadata["artist"])
|
|
if "album" in metadata and metadata["album"]:
|
|
self.albums.add(metadata["album"])
|
|
if "genre" in metadata and metadata["genre"]:
|
|
self.genres.add(metadata["genre"])
|
|
if "year" in metadata and metadata["year"]:
|
|
self.years.add(metadata["year"])
|
|
|
|
# Store in database if enabled
|
|
if self.use_db and self.db:
|
|
logger.debug(
|
|
f"Storing metadata for {metadata.get('file_path', 'unknown file')} in database"
|
|
)
|
|
db_success = self.store_metadata(metadata)
|
|
if db_success:
|
|
logger.info(
|
|
f"Successfully stored metadata for {metadata.get('file_path', 'unknown file')} in database"
|
|
)
|
|
else:
|
|
logger.warning(
|
|
f"Failed to store metadata for {metadata.get('file_path', 'unknown file')} in database"
|
|
)
|
|
|
|
def verify_database_connection(self):
|
|
"""Verify that the database connection is working properly"""
|
|
if not self.use_db or not self.db:
|
|
logger.info("Database usage is disabled")
|
|
return False
|
|
|
|
try:
|
|
# Test ping
|
|
logger.debug("Testing database connection with ping")
|
|
ping_result = self.db.ping()
|
|
if not ping_result:
|
|
logger.warning("Database ping failed")
|
|
return False
|
|
|
|
# Test basic operations
|
|
test_key = "test:connection"
|
|
test_value = "connection_test"
|
|
|
|
# Test string operations
|
|
logger.debug("Testing string operations")
|
|
string_set_result = self.db.string_ops.string_set(
|
|
test_key, test_value
|
|
)
|
|
if not string_set_result:
|
|
logger.warning("Failed to set test string in database")
|
|
return False
|
|
|
|
string_get_result = self.db.string_get(test_key)
|
|
if string_get_result != test_value:
|
|
logger.warning(
|
|
f"String get test failed. Expected '{test_value}', got '{string_get_result}'"
|
|
)
|
|
return False
|
|
|
|
# Clean up
|
|
self.db.delete(test_key)
|
|
|
|
logger.info("Database connection verified successfully")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Database verification failed with error: {e}", exc_info=True
|
|
)
|
|
return False
|