mirror of
https://github.com/PR0M3TH3AN/LBRY-Downloader.git
synced 2026-10-27 08:22:24 +00:00
274 lines
9.3 KiB
Python
274 lines
9.3 KiB
Python
"""Planner module - decides what to download based on state."""
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
from models import (
|
|
Channel,
|
|
Claim,
|
|
ClaimVersion,
|
|
Config,
|
|
DownloadAction,
|
|
StateDatabase,
|
|
)
|
|
from utils import (
|
|
create_channel_folder_name,
|
|
create_claim_folder_name,
|
|
extract_claim_metadata,
|
|
generate_version_token,
|
|
is_downloadable_claim,
|
|
)
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Planner:
|
|
"""Plans download actions by comparing discovered claims with state."""
|
|
|
|
def __init__(self, config: Config, state: StateDatabase, base_dir: str):
|
|
self.config = config
|
|
self.state = state
|
|
self.base_dir = Path(base_dir)
|
|
|
|
def process_channel_claims(
|
|
self,
|
|
channel: Channel,
|
|
claims_data: List[Dict],
|
|
) -> List[DownloadAction]:
|
|
"""
|
|
Process claims from a channel and decide what to do with each.
|
|
|
|
Args:
|
|
channel: The channel these claims belong to.
|
|
claims_data: List of claim metadata from daemon.
|
|
|
|
Returns:
|
|
List of planned download actions.
|
|
"""
|
|
actions = []
|
|
|
|
for claim_data in claims_data:
|
|
action = self._process_single_claim(channel, claim_data)
|
|
if action:
|
|
actions.append(action)
|
|
|
|
return actions
|
|
|
|
def _process_single_claim(
|
|
self,
|
|
channel: Channel,
|
|
claim_data: Dict,
|
|
) -> Optional[DownloadAction]:
|
|
"""
|
|
Process a single claim and decide what action to take.
|
|
|
|
Args:
|
|
channel: The channel this claim belongs to.
|
|
claim_data: Claim metadata from daemon.
|
|
|
|
Returns:
|
|
DownloadAction or None if claim should be skipped.
|
|
"""
|
|
# Handle wrapped response format
|
|
if "claim" in claim_data:
|
|
claim_value = claim_data["claim"]
|
|
else:
|
|
claim_value = claim_data
|
|
|
|
# Check if this is a downloadable claim
|
|
if not is_downloadable_claim(claim_value):
|
|
logger.debug(f"Skipping non-downloadable claim")
|
|
return None
|
|
|
|
# Extract metadata
|
|
try:
|
|
metadata = extract_claim_metadata(claim_value)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to extract metadata for claim: {e}")
|
|
return None
|
|
|
|
claim_id = metadata["claim_id"]
|
|
claim_name = metadata["name"]
|
|
|
|
# Skip reposts if not enabled
|
|
value_type = claim_value.get("value_type", "").lower()
|
|
if value_type == "repost" and not self.config.general.include_reposts:
|
|
logger.debug(f"Skipping repost claim {claim_name}")
|
|
return None
|
|
|
|
# Generate version token
|
|
version_token = generate_version_token(
|
|
sd_hash=metadata.get("sd_hash"),
|
|
stream_hash=metadata.get("stream_hash"),
|
|
txid=metadata.get("txid"),
|
|
nout=metadata.get("nout"),
|
|
metadata=metadata,
|
|
)
|
|
|
|
# Build paths
|
|
channel_folder = create_channel_folder_name(
|
|
channel.channel_name, channel.channel_claim_id
|
|
)
|
|
claim_folder = create_claim_folder_name(claim_name, claim_id)
|
|
|
|
# Use custom download path if provided, otherwise use base_dir
|
|
if channel.download_path:
|
|
download_base = Path(channel.download_path)
|
|
else:
|
|
download_base = self.base_dir / "channels"
|
|
|
|
version_dir = (
|
|
download_base
|
|
/ channel_folder
|
|
/ "claims"
|
|
/ claim_folder
|
|
/ "versions"
|
|
/ version_token
|
|
)
|
|
|
|
# Check state for this claim
|
|
existing_claim = self.state.claims.get(claim_id)
|
|
|
|
if existing_claim is None:
|
|
# New claim
|
|
action = DownloadAction(
|
|
claim_id=claim_id,
|
|
claim_name=claim_name,
|
|
channel_claim_id=channel.channel_claim_id,
|
|
version_token=version_token,
|
|
action="download_new",
|
|
uri=metadata["permanent_url"],
|
|
target_dir=str(version_dir),
|
|
metadata=metadata,
|
|
)
|
|
logger.info(f"New claim: {claim_name} ({action.action})")
|
|
return action
|
|
|
|
# Check if this is a new version
|
|
if version_token in existing_claim.versions:
|
|
# We know about this version
|
|
version = existing_claim.versions[version_token]
|
|
|
|
if version.downloaded:
|
|
# Check if file still exists
|
|
if self.config.general.verify_existing_files:
|
|
if version.file_relpath:
|
|
expected_path = self.base_dir / version.file_relpath
|
|
if not expected_path.exists():
|
|
# File is missing - need to redownload
|
|
action = DownloadAction(
|
|
claim_id=claim_id,
|
|
claim_name=claim_name,
|
|
channel_claim_id=channel.channel_claim_id,
|
|
version_token=version_token,
|
|
action="redownload_missing",
|
|
uri=metadata["permanent_url"],
|
|
target_dir=str(version_dir),
|
|
metadata=metadata,
|
|
)
|
|
logger.info(f"Redownload missing: {claim_name}")
|
|
return action
|
|
|
|
# File exists - skip
|
|
action = DownloadAction(
|
|
claim_id=claim_id,
|
|
claim_name=claim_name,
|
|
channel_claim_id=channel.channel_claim_id,
|
|
version_token=version_token,
|
|
action="skip_existing",
|
|
uri=metadata["permanent_url"],
|
|
target_dir=str(version_dir),
|
|
metadata=metadata,
|
|
)
|
|
if version.file_relpath:
|
|
action.metadata["existing_file_relpath"] = version.file_relpath
|
|
logger.debug(f"Skipping existing: {claim_name}")
|
|
return action
|
|
else:
|
|
# Version exists but not downloaded - download it
|
|
action = DownloadAction(
|
|
claim_id=claim_id,
|
|
claim_name=claim_name,
|
|
channel_claim_id=channel.channel_claim_id,
|
|
version_token=version_token,
|
|
action="download_new_version",
|
|
uri=metadata["permanent_url"],
|
|
target_dir=str(version_dir),
|
|
metadata=metadata,
|
|
)
|
|
logger.info(f"Download incomplete: {claim_name}")
|
|
return action
|
|
else:
|
|
# New version of existing claim
|
|
action = DownloadAction(
|
|
claim_id=claim_id,
|
|
claim_name=claim_name,
|
|
channel_claim_id=channel.channel_claim_id,
|
|
version_token=version_token,
|
|
action="download_new_version",
|
|
uri=metadata["permanent_url"],
|
|
target_dir=str(version_dir),
|
|
metadata=metadata,
|
|
)
|
|
logger.info(f"New version: {claim_name}")
|
|
return action
|
|
|
|
def update_state_from_action(self, action: DownloadAction, success: bool) -> None:
|
|
"""
|
|
Update the state database based on a completed action.
|
|
|
|
Args:
|
|
action: The action that was executed.
|
|
success: Whether the action succeeded.
|
|
"""
|
|
if not success:
|
|
return
|
|
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
# Get or create claim record
|
|
claim = self.state.claims.get(action.claim_id)
|
|
if claim is None:
|
|
channel_folder = create_channel_folder_name(
|
|
action.channel_claim_id[:20], # We don't have channel name here
|
|
action.channel_claim_id,
|
|
)
|
|
claim_folder = create_claim_folder_name(action.claim_name, action.claim_id)
|
|
|
|
claim = Claim(
|
|
claim_id=action.claim_id,
|
|
channel_claim_id=action.channel_claim_id,
|
|
name=action.claim_name,
|
|
permanent_url=action.uri,
|
|
claim_folder=f"channels/{channel_folder}/claims/{claim_folder}",
|
|
first_seen=now,
|
|
last_seen=now,
|
|
versions={},
|
|
)
|
|
self.state.claims[action.claim_id] = claim
|
|
|
|
# Update claim metadata
|
|
claim.last_seen = now
|
|
claim.latest_version_token = action.version_token
|
|
|
|
# Get or create version record
|
|
if action.version_token not in claim.versions:
|
|
metadata = action.metadata
|
|
claim.versions[action.version_token] = ClaimVersion(
|
|
version_token=action.version_token,
|
|
txid=metadata.get("txid", ""),
|
|
nout=metadata.get("nout", 0),
|
|
sd_hash=metadata.get("sd_hash"),
|
|
stream_hash=metadata.get("stream_hash"),
|
|
published_at=metadata.get("release_time"),
|
|
)
|
|
|
|
# Update version record
|
|
version = claim.versions[action.version_token]
|
|
if success:
|
|
version.downloaded = True
|
|
# File path will be set by downloader
|