mirror of
https://github.com/shadoll/moma.git
synced 2026-08-28 03:27:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b90abb457 | ||
|
|
b3c8580789 | ||
|
|
d26cbacc03 | ||
|
|
1f87337bb4 |
@@ -647,7 +647,12 @@ except (LookupError, ValueError, AttributeError) as e:
|
|||||||
|
|
||||||
1. **Read Before Modify**: Always read files before suggesting modifications
|
1. **Read Before Modify**: Always read files before suggesting modifications
|
||||||
2. **Follow Existing Patterns**: Understand established architecture before changes
|
2. **Follow Existing Patterns**: Understand established architecture before changes
|
||||||
3. **Test Everything**: Run `uv run pytest` after all changes
|
3. **Run CI Checks After Every Code Change**: After any code modification, run the same checks as GitHub CI:
|
||||||
|
```bash
|
||||||
|
uv run pytest # all tests must pass
|
||||||
|
uv run mypy src/ --ignore-missing-imports # no type errors
|
||||||
|
```
|
||||||
|
Do NOT consider work complete until both commands succeed.
|
||||||
4. **Simplicity First**: Avoid over-engineering solutions
|
4. **Simplicity First**: Avoid over-engineering solutions
|
||||||
5. **Document Changes**: Update relevant documentation
|
5. **Document Changes**: Update relevant documentation
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -28,7 +28,7 @@ powershell -c "irm https://astral.sh/uv/install.sh | iex"
|
|||||||
uv tool install https://github.com/shadoll/moma/releases/latest/download/moma-latest.tar.gz
|
uv tool install https://github.com/shadoll/moma/releases/latest/download/moma-latest.tar.gz
|
||||||
|
|
||||||
# Specific version
|
# Specific version
|
||||||
uv tool install https://github.com/shadoll/moma/releases/download/v0.9.4/moma-0.9.4-py3-none-any.whl
|
uv tool install https://github.com/shadoll/moma/releases/download/v0.9.6/moma-0.9.6-py3-none-any.whl
|
||||||
|
|
||||||
# From PyPI (when published)
|
# From PyPI (when published)
|
||||||
uv tool install moma
|
uv tool install moma
|
||||||
@@ -40,7 +40,7 @@ uv tool install moma
|
|||||||
uv tool install --force https://github.com/shadoll/moma/releases/latest/download/moma-latest.tar.gz
|
uv tool install --force https://github.com/shadoll/moma/releases/latest/download/moma-latest.tar.gz
|
||||||
|
|
||||||
# Upgrade to a newer specific version
|
# Upgrade to a newer specific version
|
||||||
uv tool install --force https://github.com/shadoll/moma/releases/download/v0.9.4/moma-0.9.4-py3-none-any.whl
|
uv tool install --force https://github.com/shadoll/moma/releases/download/v0.9.6/moma-0.9.6-py3-none-any.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Usage
|
#### Usage
|
||||||
@@ -56,7 +56,7 @@ moma /path/to/directory # Scan specific directory
|
|||||||
pip install https://github.com/shadoll/moma/releases/latest/download/moma-latest.tar.gz
|
pip install https://github.com/shadoll/moma/releases/latest/download/moma-latest.tar.gz
|
||||||
|
|
||||||
# Specific version
|
# Specific version
|
||||||
pip install https://github.com/shadoll/moma/releases/download/v0.9.4/moma-0.9.4-py3-none-any.whl
|
pip install https://github.com/shadoll/moma/releases/download/v0.9.6/moma-0.9.6-py3-none-any.whl
|
||||||
```
|
```
|
||||||
|
|
||||||
### Method 3: Development Installation
|
### Method 3: Development Installation
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "moma"
|
name = "moma"
|
||||||
version = "0.9.4"
|
version = "0.9.6"
|
||||||
description = "Terminal-based media file renamer and metadata viewer"
|
description = "Terminal-based media file renamer and metadata viewer"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
+3
-3
@@ -211,9 +211,9 @@ class MomaApp(App):
|
|||||||
icons = {
|
icons = {
|
||||||
'mkv': '🎥', # Video camera for MKV
|
'mkv': '🎥', # Video camera for MKV
|
||||||
'mk3d': '🕹️', # Clapper board for 3D
|
'mk3d': '🕹️', # Clapper board for 3D
|
||||||
'mp4': '🎥', # Video camera
|
'mp4': '🌐', # Web
|
||||||
'mov': '🎥', # Video camera
|
'mov': '📽️', # Video camera
|
||||||
'webm': '🎥', # Video camera
|
'webm': '🌐', # Web
|
||||||
'avi': '💿', # Film frames for AVI
|
'avi': '💿', # Film frames for AVI
|
||||||
'wmv': '📀', # Video camera
|
'wmv': '📀', # Video camera
|
||||||
'm4v': '📹', # Video camera
|
'm4v': '📹', # Video camera
|
||||||
|
|||||||
@@ -227,10 +227,9 @@ class FilenameExtractor:
|
|||||||
# Check for bare resolution numbers inside brackets (e.g., [720,ukr,eng])
|
# Check for bare resolution numbers inside brackets (e.g., [720,ukr,eng])
|
||||||
bare_match = re.search(r'[\[,](\d{3,4})(?=[,\]])', normalized_name, re.IGNORECASE)
|
bare_match = re.search(r'[\[,](\d{3,4})(?=[,\]])', normalized_name, re.IGNORECASE)
|
||||||
if bare_match:
|
if bare_match:
|
||||||
height = int(bare_match.group(1))
|
bare_fc = self._get_frame_class_from_height(int(bare_match.group(1)))
|
||||||
frame_class = self._get_frame_class_from_height(height)
|
if bare_fc:
|
||||||
if frame_class:
|
return bare_fc
|
||||||
return frame_class
|
|
||||||
|
|
||||||
# If no specific resolution found, check for non-standard quality indicators
|
# If no specific resolution found, check for non-standard quality indicators
|
||||||
for indicator in NON_STANDARD_QUALITY_INDICATORS:
|
for indicator in NON_STANDARD_QUALITY_INDICATORS:
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ class MediaInfoExtractor:
|
|||||||
resolution = self.extract_resolution()
|
resolution = self.extract_resolution()
|
||||||
if not resolution:
|
if not resolution:
|
||||||
return None
|
return None
|
||||||
height, width = resolution
|
width, height = resolution
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"[{self.file_path.name}] Frame class detection - Resolution: {width}x{height}"
|
f"[{self.file_path.name}] Frame class detection - Resolution: {width}x{height}"
|
||||||
@@ -249,11 +249,14 @@ class MediaInfoExtractor:
|
|||||||
effective_height = height
|
effective_height = height
|
||||||
|
|
||||||
# First, try to match width to typical widths
|
# First, try to match width to typical widths
|
||||||
# Use a larger tolerance (10 pixels) to handle cinema/ultrawide aspect ratios
|
# Use proportional tolerance (2% of typical width, min 10px) to handle
|
||||||
|
# cinema/ultrawide aspect ratios where encoded width may differ slightly
|
||||||
|
# (e.g. 3820×1592 scope 4K → 2160p, not a non-standard 1592p)
|
||||||
width_matches = []
|
width_matches = []
|
||||||
for frame_class, info in FRAME_CLASSES.items():
|
for frame_class, info in FRAME_CLASSES.items():
|
||||||
for tw in info["typical_widths"]:
|
for tw in info["typical_widths"]:
|
||||||
if abs(width - tw) <= 10 and frame_class.endswith(scan_type):
|
width_tolerance = max(10, int(tw * 0.02))
|
||||||
|
if abs(width - tw) <= width_tolerance and frame_class.endswith(scan_type):
|
||||||
diff = abs(height - info["nominal_height"])
|
diff = abs(height - info["nominal_height"])
|
||||||
width_matches.append((frame_class, diff))
|
width_matches.append((frame_class, diff))
|
||||||
|
|
||||||
@@ -329,7 +332,10 @@ class MediaInfoExtractor:
|
|||||||
return None
|
return None
|
||||||
langs = []
|
langs = []
|
||||||
for a in tracks:
|
for a in tracks:
|
||||||
lang_code = getattr(a, "language", "und") or "und"
|
lang_code = getattr(a, "language", None)
|
||||||
|
# Skip tracks with no language tag or 'und' (undetermined)
|
||||||
|
if not lang_code or lang_code.lower() in ("und", "undefined"):
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
# Try to get the 3-letter code
|
# Try to get the 3-letter code
|
||||||
lang_obj = langcodes.Language.get(lang_code.lower())
|
lang_obj = langcodes.Language.get(lang_code.lower())
|
||||||
@@ -340,6 +346,9 @@ class MediaInfoExtractor:
|
|||||||
logger.debug(f"Invalid language code '{lang_code}': {e}")
|
logger.debug(f"Invalid language code '{lang_code}': {e}")
|
||||||
langs.append(lang_code.lower()[:3])
|
langs.append(lang_code.lower()[:3])
|
||||||
|
|
||||||
|
if not langs:
|
||||||
|
return None # No meaningful language info — let Filename extractor try
|
||||||
|
|
||||||
lang_counts = Counter(langs)
|
lang_counts = Counter(langs)
|
||||||
audio_langs = [
|
audio_langs = [
|
||||||
f"{count}{lang}" if count > 1 else lang
|
f"{count}{lang}" if count > 1 else lang
|
||||||
|
|||||||
@@ -154,5 +154,12 @@
|
|||||||
"interlaced": false,
|
"interlaced": false,
|
||||||
"expected_frame_class": "1080p",
|
"expected_frame_class": "1080p",
|
||||||
"testname": "test-mistakenly-high-height-2"
|
"testname": "test-mistakenly-high-height-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"testname": "test-2160p-scope-240",
|
||||||
|
"resolution": [3820, 1592],
|
||||||
|
"interlaced": false,
|
||||||
|
"expected_frame_class": "2160p",
|
||||||
|
"description": "4K cinema scope 2.40:1 - width slightly under 3840, height non-standard 1592"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -39,7 +39,7 @@ def test_frame_class_detection(test_case):
|
|||||||
extractor.video_tracks = [mock_track]
|
extractor.video_tracks = [mock_track]
|
||||||
extractor._get_tracks.return_value = [mock_track] # satisfies @requires_tracks_type decorator
|
extractor._get_tracks.return_value = [mock_track] # satisfies @requires_tracks_type decorator
|
||||||
extractor._get_track.return_value = mock_track
|
extractor._get_track.return_value = mock_track
|
||||||
extractor.extract_resolution.return_value = (height, width)
|
extractor.extract_resolution.return_value = (width, height)
|
||||||
extractor.extract_interlaced.return_value = interlaced
|
extractor.extract_interlaced.return_value = interlaced
|
||||||
|
|
||||||
# Test the method
|
# Test the method
|
||||||
|
|||||||
Reference in New Issue
Block a user