Fbrowser/dbman.py
Stan44 a69391b1d8 toggle logging added (--debug-on) to enable logging
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.
2025-04-11 23:59:50 -05:00

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