initial comment
This commit is contained in:
345
dbmanage.py
Normal file
345
dbmanage.py
Normal file
@@ -0,0 +1,345 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Database Management Utility for encoderPro
|
||||
Provides convenient commands for inspecting and managing the state database
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class DatabaseManager:
|
||||
"""Utility for managing the state database"""
|
||||
|
||||
def __init__(self, db_path: Path):
|
||||
self.db_path = db_path
|
||||
self.conn = None
|
||||
|
||||
if not db_path.exists():
|
||||
print(f"ERROR: Database not found: {db_path}")
|
||||
sys.exit(1)
|
||||
|
||||
self.conn = sqlite3.connect(str(db_path))
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
|
||||
def close(self):
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
|
||||
def stats(self):
|
||||
"""Show database statistics"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("DATABASE STATISTICS")
|
||||
print("="*70)
|
||||
|
||||
# Count by state
|
||||
cursor.execute("""
|
||||
SELECT state, COUNT(*) as count
|
||||
FROM files
|
||||
GROUP BY state
|
||||
ORDER BY state
|
||||
""")
|
||||
|
||||
total = 0
|
||||
for row in cursor.fetchall():
|
||||
state = row['state'].upper()
|
||||
count = row['count']
|
||||
total += count
|
||||
print(f"{state:15} {count:6}")
|
||||
|
||||
print("-"*70)
|
||||
print(f"{'TOTAL':15} {total:6}")
|
||||
|
||||
# Space statistics
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
SUM(original_size) as orig,
|
||||
SUM(encoded_size) as enc
|
||||
FROM files
|
||||
WHERE state = 'completed'
|
||||
""")
|
||||
row = cursor.fetchone()
|
||||
|
||||
if row and row['orig']:
|
||||
orig = row['orig']
|
||||
enc = row['enc'] or 0
|
||||
saved = orig - enc
|
||||
|
||||
print("\n" + "="*70)
|
||||
print("SPACE STATISTICS (Completed Files)")
|
||||
print("="*70)
|
||||
print(f"Original size: {self._human_size(orig)}")
|
||||
print(f"Encoded size: {self._human_size(enc)}")
|
||||
print(f"Space saved: {self._human_size(saved)} ({saved/orig*100:.1f}%)")
|
||||
|
||||
print("="*70 + "\n")
|
||||
|
||||
def list_files(self, state: str = None, limit: int = None):
|
||||
"""List files, optionally filtered by state"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
query = "SELECT * FROM files"
|
||||
params = []
|
||||
|
||||
if state:
|
||||
query += " WHERE state = ?"
|
||||
params.append(state)
|
||||
|
||||
query += " ORDER BY updated_at DESC"
|
||||
|
||||
if limit:
|
||||
query += " LIMIT ?"
|
||||
params.append(limit)
|
||||
|
||||
cursor.execute(query, params)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
if not rows:
|
||||
print("No files found")
|
||||
return
|
||||
|
||||
print(f"\nFound {len(rows)} file(s):\n")
|
||||
print(f"{'State':<12} {'Size':<12} {'Path'}")
|
||||
print("-" * 80)
|
||||
|
||||
for row in rows:
|
||||
state = row['state']
|
||||
size = self._human_size(row['original_size']) if row['original_size'] else 'N/A'
|
||||
path = row['relative_path']
|
||||
|
||||
# Truncate long paths
|
||||
if len(path) > 50:
|
||||
path = path[:47] + "..."
|
||||
|
||||
print(f"{state:<12} {size:<12} {path}")
|
||||
|
||||
def failed_files(self):
|
||||
"""Show failed files with error messages"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT relative_path, error_message, updated_at
|
||||
FROM files
|
||||
WHERE state = 'failed'
|
||||
ORDER BY updated_at DESC
|
||||
""")
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
if not rows:
|
||||
print("No failed files")
|
||||
return
|
||||
|
||||
print(f"\n{len(rows)} FAILED FILE(S):\n")
|
||||
print("="*80)
|
||||
|
||||
for row in rows:
|
||||
print(f"File: {row['relative_path']}")
|
||||
print(f"Error: {row['error_message']}")
|
||||
print(f"Date: {row['updated_at']}")
|
||||
print("-"*80)
|
||||
|
||||
def reset_state(self, from_state: str, to_state: str = 'pending'):
|
||||
"""Reset files from one state to another"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# Count affected
|
||||
cursor.execute("SELECT COUNT(*) FROM files WHERE state = ?", (from_state,))
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
if count == 0:
|
||||
print(f"No files in state '{from_state}'")
|
||||
return
|
||||
|
||||
print(f"Found {count} file(s) in state '{from_state}'")
|
||||
response = input(f"Reset to '{to_state}'? (yes/no): ")
|
||||
|
||||
if response.lower() != 'yes':
|
||||
print("Cancelled")
|
||||
return
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE files
|
||||
SET state = ?, error_message = NULL, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE state = ?
|
||||
""", (to_state, from_state))
|
||||
|
||||
self.conn.commit()
|
||||
print(f"✓ Reset {cursor.rowcount} file(s) to '{to_state}'")
|
||||
|
||||
def reset_file(self, filepath: str, to_state: str = 'pending'):
|
||||
"""Reset a specific file to a given state"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# Check if exists
|
||||
cursor.execute("SELECT * FROM files WHERE filepath = ? OR relative_path = ?",
|
||||
(filepath, filepath))
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
print(f"File not found: {filepath}")
|
||||
return
|
||||
|
||||
print(f"File: {row['relative_path']}")
|
||||
print(f"Current state: {row['state']}")
|
||||
response = input(f"Reset to '{to_state}'? (yes/no): ")
|
||||
|
||||
if response.lower() != 'yes':
|
||||
print("Cancelled")
|
||||
return
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE files
|
||||
SET state = ?, error_message = NULL, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE filepath = ? OR relative_path = ?
|
||||
""", (to_state, filepath, filepath))
|
||||
|
||||
self.conn.commit()
|
||||
print(f"✓ Reset to '{to_state}'")
|
||||
|
||||
def search(self, pattern: str):
|
||||
"""Search for files by path pattern"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT state, relative_path, original_size, has_subtitles
|
||||
FROM files
|
||||
WHERE relative_path LIKE ?
|
||||
ORDER BY relative_path
|
||||
""", (f'%{pattern}%',))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
if not rows:
|
||||
print(f"No files matching '{pattern}'")
|
||||
return
|
||||
|
||||
print(f"\nFound {len(rows)} file(s) matching '{pattern}':\n")
|
||||
print(f"{'State':<12} {'Subs':<6} {'Size':<12} {'Path'}")
|
||||
print("-" * 80)
|
||||
|
||||
for row in rows:
|
||||
state = row['state']
|
||||
subs = 'Yes' if row['has_subtitles'] else 'No'
|
||||
size = self._human_size(row['original_size']) if row['original_size'] else 'N/A'
|
||||
path = row['relative_path']
|
||||
|
||||
if len(path) > 45:
|
||||
path = path[:42] + "..."
|
||||
|
||||
print(f"{state:<12} {subs:<6} {size:<12} {path}")
|
||||
|
||||
def vacuum(self):
|
||||
"""Vacuum (optimize) the database"""
|
||||
print("Vacuuming database...")
|
||||
self.conn.execute("VACUUM")
|
||||
print("✓ Database optimized")
|
||||
|
||||
def _human_size(self, size: int) -> str:
|
||||
"""Convert bytes to human readable format"""
|
||||
if not size:
|
||||
return "0 B"
|
||||
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if size < 1024.0:
|
||||
return f"{size:.2f} {unit}"
|
||||
size /= 1024.0
|
||||
return f"{size:.2f} PB"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Database Management Utility for encoderPro',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s stats # Show statistics
|
||||
%(prog)s list -s completed -n 10 # List 10 most recent completed files
|
||||
%(prog)s failed # Show failed files with errors
|
||||
%(prog)s reset -f failed -t pending # Reset all failed files to pending
|
||||
%(prog)s reset-file movie.mkv # Reset specific file to pending
|
||||
%(prog)s search "Avatar" # Search for files with "Avatar" in path
|
||||
%(prog)s vacuum # Optimize database
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-d', '--database',
|
||||
type=Path,
|
||||
default=Path('/var/lib/encoderpro/state.db'),
|
||||
help='Path to database file (default: /var/lib/encoderpro/state.db)'
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='Command to run')
|
||||
|
||||
# Stats command
|
||||
subparsers.add_parser('stats', help='Show database statistics')
|
||||
|
||||
# List command
|
||||
list_parser = subparsers.add_parser('list', help='List files')
|
||||
list_parser.add_argument('-s', '--state', help='Filter by state')
|
||||
list_parser.add_argument('-n', '--limit', type=int, help='Limit number of results')
|
||||
|
||||
# Failed command
|
||||
subparsers.add_parser('failed', help='Show failed files with errors')
|
||||
|
||||
# Reset command
|
||||
reset_parser = subparsers.add_parser('reset', help='Reset files from one state to another')
|
||||
reset_parser.add_argument('-f', '--from-state', required=True, help='Source state')
|
||||
reset_parser.add_argument('-t', '--to-state', default='pending', help='Target state (default: pending)')
|
||||
|
||||
# Reset file command
|
||||
reset_file_parser = subparsers.add_parser('reset-file', help='Reset a specific file')
|
||||
reset_file_parser.add_argument('filepath', help='File path (absolute or relative)')
|
||||
reset_file_parser.add_argument('-t', '--to-state', default='pending', help='Target state (default: pending)')
|
||||
|
||||
# Search command
|
||||
search_parser = subparsers.add_parser('search', help='Search for files by path pattern')
|
||||
search_parser.add_argument('pattern', help='Search pattern (case-insensitive)')
|
||||
|
||||
# Vacuum command
|
||||
subparsers.add_parser('vacuum', help='Optimize database')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
# Create manager
|
||||
db = DatabaseManager(args.database)
|
||||
|
||||
try:
|
||||
# Execute command
|
||||
if args.command == 'stats':
|
||||
db.stats()
|
||||
|
||||
elif args.command == 'list':
|
||||
db.list_files(args.state, args.limit)
|
||||
|
||||
elif args.command == 'failed':
|
||||
db.failed_files()
|
||||
|
||||
elif args.command == 'reset':
|
||||
db.reset_state(args.from_state, args.to_state)
|
||||
|
||||
elif args.command == 'reset-file':
|
||||
db.reset_file(args.filepath, args.to_state)
|
||||
|
||||
elif args.command == 'search':
|
||||
db.search(args.pattern)
|
||||
|
||||
elif args.command == 'vacuum':
|
||||
db.vacuum()
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user