chore: snapshot current project updates
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Duplicate group resolution: choose which file to keep when consuming analysis."""
|
||||
|
||||
from datetime import datetime
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
@@ -7,6 +8,14 @@ from typing import Optional, Union
|
||||
from vlm.models import MovieIdentity, SeriesIdentity
|
||||
|
||||
|
||||
def _is_sample_path(path: Path) -> bool:
|
||||
"""Identify likely sample clips by path component or filename token."""
|
||||
parts = [part.casefold() for part in path.parts]
|
||||
if "sample" in parts:
|
||||
return True
|
||||
return bool(re.search(r"(^|[\s._-])sample($|[\s._-])", path.stem.casefold()))
|
||||
|
||||
|
||||
def choose_keep_index(
|
||||
items: list[tuple[Path, Union[MovieIdentity, SeriesIdentity]]],
|
||||
strategy: str,
|
||||
@@ -17,13 +26,14 @@ def choose_keep_index(
|
||||
Strategies:
|
||||
- by_quality: Prefer higher quality (resolution > source > codec > size). Requires quality_comparison.
|
||||
- by_reputation: Prefer items with reputation_score; then sort by score desc,
|
||||
then reputation_votes desc, then first_seen (input order). Keep index 0 after sort.
|
||||
then reputation_votes desc, then quality, then first_seen (input order).
|
||||
- by_reputation_quality_time: Prefer reputation first, then quality, then newer modified time.
|
||||
- first_seen: Keep the first item (index 0).
|
||||
- manual: Return None; caller should not generate quarantine ops, only record in metadata.
|
||||
|
||||
Args:
|
||||
items: List of (path, identity) for the duplicate group.
|
||||
strategy: One of "by_quality", "by_reputation", "first_seen", "manual".
|
||||
strategy: One of "by_quality", "by_reputation", "by_reputation_quality_time", "first_seen", "manual".
|
||||
quality_comparison: List of quality dicts (filename, path, size_bytes, resolution?, codec?)
|
||||
aligned with items. Required when strategy is "by_quality".
|
||||
|
||||
@@ -39,7 +49,9 @@ def choose_keep_index(
|
||||
return 0 # Fallback to first if quality data missing/mismatched
|
||||
return _by_quality_index(items, quality_comparison)
|
||||
if strategy == "by_reputation":
|
||||
return _by_reputation_index(items)
|
||||
return _by_reputation_index(items, quality_comparison=quality_comparison)
|
||||
if strategy == "by_reputation_quality_time":
|
||||
return _by_reputation_quality_time_index(items, quality_comparison=quality_comparison)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -101,9 +113,7 @@ def _by_quality_index(
|
||||
quality_comparison: list[dict],
|
||||
) -> int:
|
||||
"""Sort by: resolution desc, source desc, codec desc, size desc, index asc. Return best index."""
|
||||
def key(idx_item: tuple[int, tuple[Path, Union[MovieIdentity, SeriesIdentity]]]) -> tuple:
|
||||
idx, (path, _) = idx_item
|
||||
qc = quality_comparison[idx] if idx < len(quality_comparison) else {}
|
||||
def quality_key(path: Path, qc: dict, idx: int) -> tuple:
|
||||
resolution = qc.get("resolution")
|
||||
codec = qc.get("codec")
|
||||
size = qc.get("size_bytes", 0) or 0
|
||||
@@ -112,6 +122,12 @@ def _by_quality_index(
|
||||
codec_tier = _parse_codec_tier(codec, path)
|
||||
return (-res_tier, -src_tier, -codec_tier, -size, idx)
|
||||
|
||||
def key(idx_item: tuple[int, tuple[Path, Union[MovieIdentity, SeriesIdentity]]]) -> tuple:
|
||||
idx, (path, _) = idx_item
|
||||
qc = quality_comparison[idx] if idx < len(quality_comparison) else {}
|
||||
is_sample = _is_sample_path(path)
|
||||
return (is_sample, *quality_key(path, qc, idx))
|
||||
|
||||
indexed = list(enumerate(items))
|
||||
indexed.sort(key=key)
|
||||
return indexed[0][0]
|
||||
@@ -119,15 +135,80 @@ def _by_quality_index(
|
||||
|
||||
def _by_reputation_index(
|
||||
items: list[tuple[Path, Union[MovieIdentity, SeriesIdentity]]],
|
||||
quality_comparison: Optional[list[dict]] = None,
|
||||
) -> int:
|
||||
"""Sort by: has reputation > no reputation; then score desc; then votes desc; then order. Return 0."""
|
||||
"""Sort by: sample, reputation, quality, then input order."""
|
||||
quality_list = quality_comparison or []
|
||||
|
||||
def quality_key(path: Path, qc: dict, idx: int) -> tuple:
|
||||
resolution = qc.get("resolution")
|
||||
codec = qc.get("codec")
|
||||
size = qc.get("size_bytes", 0) or 0
|
||||
res_tier = _parse_resolution_tier(resolution, path)
|
||||
src_tier = _parse_source_tier(path)
|
||||
codec_tier = _parse_codec_tier(codec, path)
|
||||
# Reputation fallback: prioritize source first (e.g. BluRay over WEB-DL).
|
||||
return (-src_tier, -res_tier, -codec_tier, -size, idx)
|
||||
|
||||
def key(idx_reason: tuple[int, tuple[Path, Union[MovieIdentity, SeriesIdentity]]]) -> tuple:
|
||||
idx, (path, identity) = idx_reason
|
||||
qc = quality_list[idx] if idx < len(quality_list) else {}
|
||||
has_rep = identity.reputation_score is not None
|
||||
score = identity.reputation_score if identity.reputation_score is not None else -1.0
|
||||
votes = identity.reputation_votes if identity.reputation_votes is not None else -1
|
||||
# Prefer has reputation (True > False), then higher score, then higher votes, then lower index
|
||||
return (not has_rep, -score, -votes, idx)
|
||||
is_sample = _is_sample_path(path)
|
||||
# Prefer non-sample, then reputation, then better quality, then lower index.
|
||||
return (is_sample, not has_rep, -score, -votes, *quality_key(path, qc, idx))
|
||||
|
||||
indexed = list(enumerate(items))
|
||||
indexed.sort(key=key)
|
||||
return indexed[0][0]
|
||||
|
||||
|
||||
def _parse_modified_timestamp(value: Optional[str]) -> float:
|
||||
"""Parse ISO modified timestamp to unix epoch seconds; unknown returns 0."""
|
||||
if not value:
|
||||
return 0.0
|
||||
try:
|
||||
normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
|
||||
return datetime.fromisoformat(normalized).timestamp()
|
||||
except (ValueError, TypeError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _by_reputation_quality_time_index(
|
||||
items: list[tuple[Path, Union[MovieIdentity, SeriesIdentity]]],
|
||||
quality_comparison: Optional[list[dict]] = None,
|
||||
) -> int:
|
||||
"""Sort by: sample, reputation, quality, modified time (newer first), then order."""
|
||||
quality_list = quality_comparison or []
|
||||
|
||||
def quality_key(path: Path, qc: dict) -> tuple:
|
||||
resolution = qc.get("resolution")
|
||||
codec = qc.get("codec")
|
||||
size = qc.get("size_bytes", 0) or 0
|
||||
res_tier = _parse_resolution_tier(resolution, path)
|
||||
src_tier = _parse_source_tier(path)
|
||||
codec_tier = _parse_codec_tier(codec, path)
|
||||
return (-res_tier, -src_tier, -codec_tier, -size)
|
||||
|
||||
def key(idx_reason: tuple[int, tuple[Path, Union[MovieIdentity, SeriesIdentity]]]) -> tuple:
|
||||
idx, (path, identity) = idx_reason
|
||||
qc = quality_list[idx] if idx < len(quality_list) else {}
|
||||
has_rep = identity.reputation_score is not None
|
||||
score = identity.reputation_score if identity.reputation_score is not None else -1.0
|
||||
votes = identity.reputation_votes if identity.reputation_votes is not None else -1
|
||||
modified_ts = _parse_modified_timestamp(qc.get("modified_timestamp"))
|
||||
is_sample = _is_sample_path(path)
|
||||
return (
|
||||
is_sample,
|
||||
not has_rep,
|
||||
-score,
|
||||
-votes,
|
||||
*quality_key(path, qc),
|
||||
-modified_ts,
|
||||
idx,
|
||||
)
|
||||
|
||||
indexed = list(enumerate(items))
|
||||
indexed.sort(key=key)
|
||||
|
||||
Reference in New Issue
Block a user