Initial commit
This commit is contained in:
commit
1d235d30e7
58 files changed
+19693
No files matched your search
@@ -0,0 +1,312 @@
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
from media_sorter.analyzer import MediaMetadata, StreamInfo
|
||||
from media_sorter.classifier import MediaClassifier
|
||||
from media_sorter.providers import MockMetadataProvider, ProviderResult
|
||||
from media_sorter.scanner import ScannedFile
|
||||
from media_sorter.tokenizer import FilenameTokenizer, TokenizedFilename
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def classifier():
|
||||
return MediaClassifier(confidence_threshold=0.75)
|
||||
|
||||
|
||||
def test_classify_tv_show(classifier):
|
||||
scanned = ScannedFile(path=Path("/downloads/Game.of.Thrones.S01E01.1080p.mkv"), size=1000000, mtime=1000.0)
|
||||
tokens = TokenizedFilename(
|
||||
raw_name="Game.of.Thrones.S01E01.1080p.mkv",
|
||||
title="Game of Thrones",
|
||||
season=1,
|
||||
episode=1,
|
||||
is_episodic=True,
|
||||
)
|
||||
meta = MediaMetadata(
|
||||
path=scanned.path,
|
||||
mime_type="video/x-matroska",
|
||||
container="mkv",
|
||||
duration_seconds=3600,
|
||||
has_video=True,
|
||||
)
|
||||
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.category == "tv"
|
||||
assert res.confidence >= 0.75
|
||||
assert res.needs_quarantine is False
|
||||
|
||||
|
||||
def test_classify_anime(classifier):
|
||||
scanned = ScannedFile(path=Path("/downloads/[SubsPlease] Jujutsu Kaisen - 01 [1080p].mkv"), size=1000000, mtime=1000.0)
|
||||
tokens = TokenizedFilename(
|
||||
raw_name="[SubsPlease] Jujutsu Kaisen - 01 [1080p].mkv",
|
||||
title="Jujutsu Kaisen",
|
||||
episode=1,
|
||||
season=1,
|
||||
group="SubsPlease",
|
||||
is_anime=True,
|
||||
is_episodic=True,
|
||||
)
|
||||
meta = MediaMetadata(
|
||||
path=scanned.path,
|
||||
mime_type="video/x-matroska",
|
||||
container="mkv",
|
||||
duration_seconds=1400,
|
||||
has_video=True,
|
||||
)
|
||||
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.category == "anime"
|
||||
assert res.confidence >= 0.75
|
||||
assert res.needs_quarantine is False
|
||||
|
||||
|
||||
def test_classify_movie(classifier):
|
||||
scanned = ScannedFile(path=Path("/downloads/Interstellar.2014.1080p.mkv"), size=5000000, mtime=1000.0)
|
||||
tokens = TokenizedFilename(
|
||||
raw_name="Interstellar.2014.1080p.mkv",
|
||||
title="Interstellar",
|
||||
year=2014,
|
||||
resolution="1080p",
|
||||
)
|
||||
meta = MediaMetadata(
|
||||
path=scanned.path,
|
||||
mime_type="video/x-matroska",
|
||||
container="mkv",
|
||||
duration_seconds=10140, # ~2.8 hours
|
||||
has_video=True,
|
||||
)
|
||||
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.category == "movie"
|
||||
assert res.confidence >= 0.75
|
||||
assert res.needs_quarantine is False
|
||||
|
||||
|
||||
def test_classify_music(classifier):
|
||||
scanned = ScannedFile(path=Path("/music/01 - Come Together.flac"), size=30000000, mtime=1000.0)
|
||||
tokens = TokenizedFilename(
|
||||
raw_name="01 - Come Together.flac",
|
||||
title="Come Together",
|
||||
track=1,
|
||||
is_music=True,
|
||||
)
|
||||
meta = MediaMetadata(
|
||||
path=scanned.path,
|
||||
mime_type="audio/flac",
|
||||
container="flac",
|
||||
duration_seconds=259,
|
||||
has_audio=True,
|
||||
has_video=False,
|
||||
tags={"artist": "The Beatles", "album": "Abbey Road"},
|
||||
)
|
||||
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.category == "music"
|
||||
assert res.confidence >= 0.75
|
||||
assert res.needs_quarantine is False
|
||||
|
||||
|
||||
def test_classify_audiobook(classifier):
|
||||
scanned = ScannedFile(path=Path("/audiobooks/Dune - Part 01.m4b"), size=50000000, mtime=1000.0)
|
||||
tokens = TokenizedFilename(raw_name="Dune - Part 01.m4b", title="Dune")
|
||||
meta = MediaMetadata(
|
||||
path=scanned.path,
|
||||
mime_type="audio/mp4",
|
||||
container="m4b",
|
||||
duration_seconds=28800, # 8 hours
|
||||
has_audio=True,
|
||||
has_video=False,
|
||||
tags={"narrator": "George Guidall"},
|
||||
)
|
||||
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.category == "audiobook"
|
||||
assert res.confidence >= 0.75
|
||||
assert res.needs_quarantine is False
|
||||
|
||||
|
||||
def test_low_confidence_triggers_quarantine(classifier):
|
||||
# Ambiguous video clip with no year, no episode, no metadata
|
||||
scanned = ScannedFile(path=Path("/incoming/unknown_recording_xyz.mkv"), size=10000, mtime=1000.0)
|
||||
tokens = TokenizedFilename(raw_name="unknown_recording_xyz.mkv", title="unknown recording xyz")
|
||||
meta = MediaMetadata(
|
||||
path=scanned.path,
|
||||
mime_type="video/x-matroska",
|
||||
container="mkv",
|
||||
duration_seconds=120,
|
||||
has_video=True,
|
||||
)
|
||||
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.confidence < 0.75
|
||||
assert res.needs_quarantine is True
|
||||
assert res.quarantine_reason is not None
|
||||
|
||||
|
||||
def test_unsupported_format_triggers_quarantine(classifier):
|
||||
scanned = ScannedFile(path=Path("/incoming/corrupt_data.bin"), size=1000, mtime=1000.0)
|
||||
tokens = TokenizedFilename(raw_name="corrupt_data.bin")
|
||||
meta = MediaMetadata(path=scanned.path, mime_type="application/octet-stream", container="bin")
|
||||
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.category == "unknown"
|
||||
assert res.needs_quarantine is True
|
||||
|
||||
|
||||
def test_classify_with_metadata_provider():
|
||||
mock_prov = MockMetadataProvider(
|
||||
mock_data={
|
||||
"movie:oppenheimer": ProviderResult(
|
||||
canonical_title="Oppenheimer",
|
||||
year=2023,
|
||||
media_type="movie",
|
||||
confidence_boost=0.15,
|
||||
),
|
||||
"tv:the last of us": ProviderResult(
|
||||
canonical_title="The Last of Us",
|
||||
year=2023,
|
||||
media_type="tv",
|
||||
season=1,
|
||||
episode=3,
|
||||
episode_title="Long, Long Time",
|
||||
confidence_boost=0.20,
|
||||
),
|
||||
}
|
||||
)
|
||||
prov_classifier = MediaClassifier(confidence_threshold=0.75, provider=mock_prov)
|
||||
|
||||
# 1. Movie verified with provider
|
||||
scanned_m = ScannedFile(path=Path("/downloads/Oppenheimer.mkv"), size=1000000, mtime=1000.0)
|
||||
tokens_m = TokenizedFilename(raw_name="Oppenheimer.mkv", title="Oppenheimer")
|
||||
meta_m = MediaMetadata(path=scanned_m.path, mime_type="video/x-matroska", container="mkv", duration_seconds=10800, has_video=True)
|
||||
res_m = prov_classifier.classify(scanned_m, tokens_m, meta_m)
|
||||
assert res_m.category == "movie"
|
||||
assert res_m.provider_result is not None
|
||||
assert res_m.provider_result.year == 2023
|
||||
|
||||
# 2. TV Show verified with provider
|
||||
scanned_tv = ScannedFile(path=Path("/downloads/The.Last.of.Us.S01E03.mkv"), size=1000000, mtime=1000.0)
|
||||
tokens_tv = TokenizedFilename(raw_name="The.Last.of.Us.S01E03.mkv", title="The Last of Us", season=1, episode=3, is_episodic=True)
|
||||
meta_tv = MediaMetadata(path=scanned_tv.path, mime_type="video/x-matroska", container="mkv", duration_seconds=4500, has_video=True)
|
||||
res_tv = prov_classifier.classify(scanned_tv, tokens_tv, meta_tv)
|
||||
assert res_tv.category == "tv"
|
||||
assert res_tv.provider_result is not None
|
||||
assert res_tv.provider_result.episode_title == "Long, Long Time"
|
||||
|
||||
|
||||
def test_classify_podcast(classifier):
|
||||
scanned = ScannedFile(path=Path("/podcasts/Hardcore History 2023-05-12 Episode 68.mp3"), size=50000000, mtime=1000.0)
|
||||
tokens = TokenizedFilename(raw_name="Hardcore History 2023-05-12 Episode 68.mp3", title="Episode 68", date_stamp="2023-05-12")
|
||||
meta = MediaMetadata(
|
||||
path=scanned.path,
|
||||
mime_type="audio/mpeg",
|
||||
container="mp3",
|
||||
duration_seconds=14400,
|
||||
has_audio=True,
|
||||
has_video=False,
|
||||
tags={"podcast": "Dan Carlin's Hardcore History"},
|
||||
)
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.category == "podcast"
|
||||
assert res.confidence >= 0.75
|
||||
|
||||
|
||||
def test_classify_photo_and_home_video(classifier):
|
||||
# Photo test
|
||||
scanned_p = ScannedFile(path=Path("/photos/IMG_20250615_123456.jpg"), size=4000000, mtime=1000.0)
|
||||
tokens_p = TokenizedFilename(raw_name="IMG_20250615_123456.jpg", is_photo_or_home_video=True, date_stamp="2025-06-15")
|
||||
meta_p = MediaMetadata(path=scanned_p.path, mime_type="image/jpeg", container="jpeg", tags={"camera_model": "Pixel 9 Pro"})
|
||||
res_p = classifier.classify(scanned_p, tokens_p, meta_p)
|
||||
assert res_p.category == "photo"
|
||||
assert res_p.confidence >= 0.90
|
||||
|
||||
# Home Video test
|
||||
scanned_v = ScannedFile(path=Path("/home_videos/VID_20250615_140000.mp4"), size=20000000, mtime=1000.0)
|
||||
tokens_v = TokenizedFilename(raw_name="VID_20250615_140000.mp4", is_photo_or_home_video=True, date_stamp="2025-06-15")
|
||||
meta_v = MediaMetadata(path=scanned_v.path, mime_type="video/mp4", container="mp4", duration_seconds=120, has_video=True)
|
||||
res_v = classifier.classify(scanned_v, tokens_v, meta_v)
|
||||
assert res_v.category == "home_video"
|
||||
|
||||
|
||||
def test_classify_archive(classifier):
|
||||
scanned = ScannedFile(path=Path("/downloads/Season1_Extras.zip"), size=500000000, mtime=1000.0)
|
||||
tokens = TokenizedFilename(raw_name="Season1_Extras.zip")
|
||||
meta = MediaMetadata(path=scanned.path, mime_type="application/zip", container="zip")
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.category == "archive"
|
||||
assert res.confidence >= 0.90
|
||||
|
||||
|
||||
def test_classify_movie_with_hdtv_and_rartv(classifier):
|
||||
scanned = ScannedFile(path=Path("/downloads/Gladiator.II.2024.1080p.HDTV.x264-[rartv].mkv"), size=4000000000, mtime=1000.0)
|
||||
tokens = TokenizedFilename(
|
||||
raw_name="Gladiator.II.2024.1080p.HDTV.x264-[rartv].mkv",
|
||||
title="Gladiator II",
|
||||
year=2024,
|
||||
resolution="1080p",
|
||||
video_codec="x264",
|
||||
source="HDTV",
|
||||
group="rartv",
|
||||
)
|
||||
meta = MediaMetadata(
|
||||
path=scanned.path,
|
||||
mime_type="video/x-matroska",
|
||||
container="mkv",
|
||||
duration_seconds=5000, # ~83 minutes
|
||||
has_video=True,
|
||||
)
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.category == "movie"
|
||||
assert res.confidence >= 0.75
|
||||
assert res.needs_quarantine is False
|
||||
|
||||
|
||||
def test_classify_movie_with_apple_tv_tag(classifier):
|
||||
scanned = ScannedFile(path=Path("/downloads/Wolfs.2024.1080p.Apple.TV.WEB-DL.DDP5.1.Atmos.H.264.mkv"), size=4500000000, mtime=1000.0)
|
||||
tokens = TokenizedFilename(
|
||||
raw_name="Wolfs.2024.1080p.Apple.TV.WEB-DL.DDP5.1.Atmos.H.264.mkv",
|
||||
title="Wolfs",
|
||||
year=2024,
|
||||
resolution="1080p",
|
||||
video_codec="H.264",
|
||||
source="WEB-DL",
|
||||
)
|
||||
meta = MediaMetadata(
|
||||
path=scanned.path,
|
||||
mime_type="video/x-matroska",
|
||||
container="mkv",
|
||||
duration_seconds=6400,
|
||||
has_video=True,
|
||||
)
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.category == "movie"
|
||||
assert res.confidence >= 0.75
|
||||
|
||||
|
||||
def test_video_file_with_audio_not_classified_as_music(classifier):
|
||||
# Video container .mkv with audio track should never be classified as music
|
||||
scanned = ScannedFile(
|
||||
path=Path("/downloads/Star.Wars.The.Clone.Wars.S01E01.1080p.BluRay.REMUX.VC-1.DD5.1-NOGRP.mkv"),
|
||||
size=4500000000,
|
||||
mtime=1000.0,
|
||||
)
|
||||
tokens = TokenizedFilename(
|
||||
raw_name="Star.Wars.The.Clone.Wars.S01E01.1080p.BluRay.REMUX.VC-1.DD5.1-NOGRP.mkv",
|
||||
title="Star Wars The Clone Wars",
|
||||
season=1,
|
||||
episode=1,
|
||||
is_episodic=True,
|
||||
)
|
||||
meta = MediaMetadata(
|
||||
path=scanned.path,
|
||||
mime_type="video/x-matroska",
|
||||
container="mkv",
|
||||
has_audio=True,
|
||||
has_video=False, # e.g. exotic codec in container
|
||||
)
|
||||
res = classifier.classify(scanned, tokens, meta)
|
||||
assert res.category == "tv"
|
||||
assert res.confidence >= 0.80
|
||||
assert res.needs_quarantine is False
|
||||
|
||||
|
||||
Reference in new issue
Block a user