[BREAKING CHANGES] Refactor media file caching with nested directory structure and improved metadata tracking

This commit is contained in:
vvzvlad
2025-02-07 01:22:06 +03:00
parent 0ea7717422
commit d335f585ce
2 changed files with 104 additions and 72 deletions
+60 -29
View File
@@ -32,6 +32,10 @@ Config = get_settings()
@asynccontextmanager
async def lifespan(_: FastAPI):
# Create cache directory
base_cache_dir = os.path.abspath("./data/cache")
os.makedirs(base_cache_dir, exist_ok=True)
await client.start()
background_task = asyncio.create_task(cache_media_files()) # Start background task
yield
@@ -117,8 +121,12 @@ async def download_media_file(channel: str, post_id: int, file_unique_id: str) -
Download media file from Telegram and save to cache
Returns path to downloaded file
"""
cache_dir = os.path.abspath("./data/cache")
os.makedirs(cache_dir, exist_ok=True)
base_cache_dir = os.path.abspath("./data/cache")
# Create nested cache structure
channel_dir = os.path.join(base_cache_dir, str(channel))
post_dir = os.path.join(channel_dir, str(post_id))
os.makedirs(post_dir, exist_ok=True)
# Convert numeric channel ID to int if needed
channel_id = channel
@@ -139,8 +147,8 @@ async def download_media_file(channel: str, post_id: int, file_unique_id: str) -
logger.error(f"Failed to get video file size for message {post_id}: {str(e)}")
if is_large_video:
# For large video, download without permanent caching; use a temporary file.
temp_file_path = os.path.join(cache_dir, f"temp_{channel}-{post_id}-{file_unique_id}")
# For large video, download without permanent caching; use a temporary file
temp_file_path = os.path.join(post_dir, f"temp_{file_unique_id}")
if os.path.exists(temp_file_path):
logger.info(f"Temporary file {temp_file_path} already exists, serving cached large video")
return temp_file_path, False
@@ -154,23 +162,25 @@ async def download_media_file(channel: str, post_id: int, file_unique_id: str) -
return file_path, False
# Normal caching flow
cache_path = os.path.join(cache_dir, f"{channel}-{post_id}-{file_unique_id}")
cache_path = os.path.join(post_dir, file_unique_id)
if os.path.exists(cache_path):
logger.info(f"Found cached media file: {cache_path}")
file_id = f"{channel}-{post_id}-{file_unique_id}"
# Update access timestamp in media_file_ids.json
file_ids_path = os.path.join(os.path.abspath("./data"), 'media_file_ids.json')
try:
if os.path.exists(file_ids_path):
with open(file_ids_path, 'r', encoding='utf-8') as f:
media_files = json.load(f)
for file_data in media_files:
if file_data['file_id'] == file_id:
if (file_data.get('channel') == channel and
file_data.get('post_id') == post_id and
file_data.get('file_unique_id') == file_unique_id):
file_data['added'] = datetime.now().timestamp()
break
with open(file_ids_path, 'w', encoding='utf-8') as f:
json.dump(media_files, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"Failed to update timestamp for {file_id}: {str(e)}")
logger.error(f"Failed to update timestamp for {channel}/{post_id}/{file_unique_id}: {str(e)}")
return cache_path, False
file_id = await find_file_id_in_message(message, file_unique_id)
@@ -248,29 +258,24 @@ async def download_new_files(media_files: list, cache_dir: str):
files_to_download = 0
for file_data in media_files:
try:
file_id = file_data['file_id']
if not file_id:
continue
# Split by last two hyphens to handle negative channel IDs correctly
last_hyphen_pos = file_id.rindex('-')
second_last_hyphen_pos = file_id.rindex('-', 0, last_hyphen_pos)
channel = file_data.get('channel')
post_id = file_data.get('post_id')
file_unique_id = file_data.get('file_unique_id')
channel = file_id[:second_last_hyphen_pos]
post_id_str = file_id[second_last_hyphen_pos + 1:last_hyphen_pos]
file_unique_id = file_id[last_hyphen_pos + 1:]
if not all([channel, post_id, file_unique_id]):
logger.error(f"Invalid file data: {file_data}")
continue
post_id = int(post_id_str)
cache_path = os.path.join(cache_dir, f"{channel}-{post_id}-{file_unique_id}")
cache_path = os.path.join(cache_dir, str(channel), str(post_id), file_unique_id)
if not os.path.exists(cache_path):
files_to_download += 1
logger.debug(f"Background download started for {file_id}")
logger.debug(f"Background download started for {channel}/{post_id}/{file_unique_id}")
await download_media_file(channel, post_id, file_unique_id)
await asyncio.sleep(1) # Delay between downloads
except Exception as e:
logger.error(f"Background download failed for {file_id}: {str(e)}")
logger.error(f"Background download failed for {channel}/{post_id}/{file_unique_id}: {str(e)}")
continue
if files_to_download == 0:
@@ -312,13 +317,38 @@ async def cache_media_files():
def calculate_cache_stats():
"""
Calculate cache statistics including file count, total size in MB, and time difference in days.
Returns a dictionary with keys: 'cache_files_count', 'cache_total_size_mb', 'cache_time_diff_days'.
Returns a dictionary with keys: 'cache_files_count', 'cache_total_size_mb', 'cache_time_diff_days', 'channels'.
"""
cache_dir = os.path.abspath("./data/cache")
if os.path.isdir(cache_dir):
files = [f for f in os.listdir(cache_dir) if os.path.isfile(os.path.join(cache_dir, f))]
cache_files_count = len(files)
cache_total_size_bytes = sum(os.path.getsize(os.path.join(cache_dir, f)) for f in files)
base_cache_dir = os.path.abspath("./data/cache")
cache_files_count = 0
cache_total_size_bytes = 0
channels_stats = {}
if os.path.isdir(base_cache_dir):
# Recursively walk through all subdirectories
for root, _, files in os.walk(base_cache_dir):
for f in files:
if not f.startswith('temp_'): # Skip temporary files
file_path = os.path.join(root, f)
file_size = os.path.getsize(file_path)
cache_files_count += 1
cache_total_size_bytes += file_size
# Calculate per-channel statistics
rel_path = os.path.relpath(root, base_cache_dir)
channel = rel_path.split(os.sep)[0] # First directory is channel
if channel not in channels_stats:
channels_stats[channel] = {
'files_count': 0,
'size_mb': 0
}
channels_stats[channel]['files_count'] += 1
channels_stats[channel]['size_mb'] = round(
channels_stats[channel]['size_mb'] + (file_size / (1024 * 1024)), 2
)
cache_total_size_mb = round(cache_total_size_bytes / (1024 * 1024), 2) # rounded size in MB
else:
cache_files_count = 0
@@ -344,7 +374,8 @@ def calculate_cache_stats():
return {
"cache_files_count": cache_files_count,
"cache_total_size_mb": cache_total_size_mb,
"cache_time_diff_days": cache_time_diff_days
"cache_time_diff_days": cache_time_diff_days,
"channels": channels_stats
}
+44 -43
View File
@@ -468,7 +468,10 @@ class PostParser:
def _save_media_file_ids(self, message: Message) -> None:
try:
file_data = {
'file_id': None,
'channel': None,
'post_id': None,
'file_unique_id': None,
'added': None
}
channel_username = self.get_channel_username(message)
@@ -478,60 +481,58 @@ class PostParser:
if message.media:
if message.photo:
file_data['file_id'] = f"{channel_username}-{message.id}-{message.photo.file_unique_id}"
file_data['file_unique_id'] = message.photo.file_unique_id
elif message.video:
try:
if message.video.file_size > 100 * 1024 * 1024:
logger.info(f"Large video file for message {message.id} is not cached due to size limit")
else:
file_data['file_id'] = f"{channel_username}-{message.id}-{message.video.file_unique_id}"
except Exception as e:
logger.error(f"video_file_size_error: failed to get video file size for message {message.id}: {str(e)}")
file_data['file_id'] = f"{channel_username}-{message.id}-{message.video.file_unique_id}"
file_data['file_unique_id'] = message.video.file_unique_id
elif message.document:
file_data['file_id'] = f"{channel_username}-{message.id}-{message.document.file_unique_id}"
file_data['file_unique_id'] = message.document.file_unique_id
elif message.audio:
file_data['file_id'] = f"{channel_username}-{message.id}-{message.audio.file_unique_id}"
file_data['file_unique_id'] = message.audio.file_unique_id
elif message.voice:
file_data['file_id'] = f"{channel_username}-{message.id}-{message.voice.file_unique_id}"
file_data['file_unique_id'] = message.voice.file_unique_id
elif message.video_note:
file_data['file_id'] = f"{channel_username}-{message.id}-{message.video_note.file_unique_id}"
file_data['file_unique_id'] = message.video_note.file_unique_id
elif message.animation:
file_data['file_id'] = f"{channel_username}-{message.id}-{message.animation.file_unique_id}"
file_data['file_unique_id'] = message.animation.file_unique_id
elif message.sticker:
file_data['file_id'] = f"{channel_username}-{message.id}-{message.sticker.file_unique_id}"
file_data['file_unique_id'] = message.sticker.file_unique_id
elif message.web_page and message.web_page.photo:
file_data['file_id'] = f"{channel_username}-{message.id}-{message.web_page.photo.file_unique_id}"
file_data['file_unique_id'] = message.web_page.photo.file_unique_id
if file_data['file_id']:
file_path = os.path.join(os.path.abspath("./data"), 'media_file_ids.json')
try:
existing_data = []
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
existing_data = json.load(f)
# Check if file_id already exists
found = False
for item in existing_data:
if item['file_id'] == file_data['file_id']:
item['added'] = datetime.now().timestamp()
found = True
break
# Add new entry if not found
if not found:
file_data['added'] = datetime.now().timestamp()
existing_data.append(file_data)
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(existing_data, f, ensure_ascii=False, indent=2)
if file_data['file_unique_id']:
file_data['channel'] = channel_username
file_data['post_id'] = message.id
file_data['added'] = datetime.now().timestamp()
except Exception as e:
logger.error(f"file_id_save_error: error writing to {file_path}: {str(e)}")
file_path = os.path.join(os.path.abspath("./data"), 'media_file_ids.json')
try:
existing_data = []
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
existing_data = json.load(f)
# Check if file already exists by all three fields
found = False
for item in existing_data:
if (item.get('channel') == file_data['channel'] and
item.get('post_id') == file_data['post_id'] and
item.get('file_unique_id') == file_data['file_unique_id']):
item['added'] = datetime.now().timestamp()
found = True
break
# Add new entry if not found
if not found:
existing_data.append(file_data)
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(existing_data, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"file_id_save_error: error writing to {file_path}: {str(e)}")
except Exception as e:
logger.error(f"file_id_collection_error: message_id {message.id}, error {str(e)}")
logger.error(f"file_id_collection_error: message_id {message.id}, error {str(e)}")
def get_channel_username(self, message):
"""Extract channel username or ID from message"""