match series to tvdb episodes py
adapt serverlibrary
This commit is contained in:
1 parent
104b912404
commit
d241017c2a
3 files changed
+181
-114
No files matched your search
Regular → Executable
+76
-42
@@ -1,25 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import json
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
weight_jaccard = 0.5
|
||||
weight_fuzzy = 1 - weight_jaccard
|
||||
|
||||
from serverlibrary import (words, word_similarity, score_match_jaccard, score_match_fuzzy,
|
||||
combined_score, tmdb_get, tvdb_get)
|
||||
from serverlibrary import (
|
||||
combined_score,
|
||||
score_match_fuzzy,
|
||||
score_match_jaccard,
|
||||
score_match_recall_jaccard,
|
||||
tvdb_get,
|
||||
words,
|
||||
)
|
||||
|
||||
MATCH_THRESHOLD = 0.85
|
||||
|
||||
|
||||
class CustomLogger(logging.Logger):
|
||||
def trace(self, msg: str, *args, **kwargs) -> None:
|
||||
if self.isEnabledFor(TRACE):
|
||||
self._log(TRACE, msg, args, **kwargs)
|
||||
|
||||
|
||||
TRACE = 5
|
||||
|
||||
logging.setLoggerClass(CustomLogger)
|
||||
@@ -34,12 +42,14 @@ def normalize(text: str) -> str:
|
||||
r"\b(1080p|720p|2160p|x264|x265|h264|h265|bluray|web-dl|webrip|dvdrip)\b",
|
||||
"",
|
||||
text,
|
||||
flags=re.I,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
text = re.sub(r"[._-]", " ", text)
|
||||
text = re.sub(r"ß", "ss", text)
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
text = re.sub(r"\((\d+)\s+\d+\)", r"(\1)", text)
|
||||
text = re.sub(r"\((\d+)_+\d+\)", r"(\1)", text)
|
||||
|
||||
return text.strip()
|
||||
|
||||
@@ -70,6 +80,7 @@ def get_all_episodes_by_tvdbid(tvdb_id: int):
|
||||
|
||||
return episodes
|
||||
|
||||
|
||||
def parse_series_name(series_name: str):
|
||||
"""
|
||||
Universe (1987) [66757]
|
||||
@@ -97,7 +108,8 @@ def parse_series_name(series_name: str):
|
||||
|
||||
return title, year, tmdb_id
|
||||
|
||||
def get_tvdbid_by_name(series_name: str) -> tuple[int, Any]:
|
||||
|
||||
def get_tvdbid_by_name(series_name: str):
|
||||
title, year, tmdb_id = parse_series_name(series_name)
|
||||
|
||||
params = {
|
||||
@@ -133,20 +145,19 @@ def get_tvdbid_by_name(series_name: str) -> tuple[int, Any]:
|
||||
if filtered:
|
||||
results = filtered
|
||||
|
||||
return int(results[0]["tvdb_id"]), results[0]
|
||||
return results[0]
|
||||
|
||||
|
||||
def best_episode_match(filename, episodes):
|
||||
search_words = words(filename)
|
||||
|
||||
search_words = words(normalize(filename))
|
||||
logger.debug(f"Search Words: {search_words}")
|
||||
best_score = 0.0
|
||||
best_episode = None
|
||||
|
||||
for episode in episodes:
|
||||
logger.trace(f" Title: {episode["title"]}")
|
||||
if not episode["title"] or episode["title"] == "Unkown":
|
||||
continue
|
||||
candidate_words = words(episode["title"])
|
||||
|
||||
candidate_words = words(normalize(episode["title"]))
|
||||
fuzzy = score_match_fuzzy(
|
||||
search_words,
|
||||
candidate_words,
|
||||
@@ -155,6 +166,14 @@ def best_episode_match(filename, episodes):
|
||||
search_words,
|
||||
candidate_words,
|
||||
)
|
||||
recall_jaccard1 = score_match_recall_jaccard(
|
||||
search_words,
|
||||
candidate_words,
|
||||
)
|
||||
recall_jaccard2 = score_match_recall_jaccard(
|
||||
candidate_words,
|
||||
search_words,
|
||||
)
|
||||
|
||||
score = combined_score(
|
||||
fuzzy,
|
||||
@@ -162,8 +181,15 @@ def best_episode_match(filename, episodes):
|
||||
weight_fuzzy=weight_fuzzy,
|
||||
weight_jaccard=weight_jaccard,
|
||||
)
|
||||
score = max(score, recall_jaccard1, recall_jaccard2)
|
||||
|
||||
if "hyänen" in search_words and score > 0.6:
|
||||
logger.warning(
|
||||
f"SEARCH WORDS: {search_words}, CANDIDATE WORDS: {candidate_words}"
|
||||
)
|
||||
|
||||
if score > best_score:
|
||||
logger.trace(f"{search_words}:\t\t{candidate_words},\t\t{score}")
|
||||
best_score = score
|
||||
best_episode = episode
|
||||
|
||||
@@ -173,45 +199,55 @@ def best_episode_match(filename, episodes):
|
||||
def process_series(series_path: Path, dry_run: bool = False):
|
||||
series_name = series_path.name
|
||||
logger.debug(f"Pricess series with name : {series_name}")
|
||||
#tvdb_id, media_data = get_tvdbid_by_name(series_name)
|
||||
# episodes = get_all_episodes_by_tvdbid(tvdb_id)
|
||||
#with open("media_data.json", "w", encoding="utf-8") as f:
|
||||
# json.dump(media_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
with open("media_data.json", encoding="utf-8") as f:
|
||||
media_data = json.load(f)
|
||||
with open("episodes.json", encoding="utf-8") as f:
|
||||
episodes = json.load(f)
|
||||
|
||||
|
||||
logger.info(f"Found series name {media_data["name"]}")
|
||||
if Path(f"{series_name}_episodes.json").is_file():
|
||||
logger.info("load json files")
|
||||
with open(f"{series_name}_media_data.json", encoding="utf-8") as f:
|
||||
media_data = json.load(f)
|
||||
with open(f"{series_name}_episodes.json", encoding="utf-8") as f:
|
||||
episodes = json.load(f)
|
||||
tvdb_id = int(media_data["tvdb_id"])
|
||||
for ep in episodes:
|
||||
logger.debug(f"{ep.get('title')}")
|
||||
else:
|
||||
logger.info("QUery from TVDB")
|
||||
media_data = get_tvdbid_by_name(series_name)
|
||||
tvdb_id = int(media_data["tvdb_id"])
|
||||
episodes = get_all_episodes_by_tvdbid(tvdb_id)
|
||||
logger.info("Dump to json")
|
||||
with open(f"{series_name}_media_data.json", "w", encoding="utf-8") as f:
|
||||
json.dump(media_data, f, ensure_ascii=False, indent=2)
|
||||
with open(f"{series_name}_episodes.json", "w", encoding="utf-8") as f:
|
||||
json.dump(episodes, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"Found series name {media_data['name']}")
|
||||
extra_episodes_path = series_path / "Extra Episodes"
|
||||
|
||||
for file in extra_episodes_path.iterdir():
|
||||
if not file.is_file():
|
||||
continue
|
||||
|
||||
filename = normalize(re.sub( media_data["name"],"", file.stem))
|
||||
filename = re.sub(media_data["name"], "", file.stem)
|
||||
|
||||
episode, score = best_episode_match(
|
||||
filename,
|
||||
episodes,
|
||||
)
|
||||
|
||||
if episode is None:
|
||||
continue
|
||||
|
||||
if score < MATCH_THRESHOLD:
|
||||
logger.warning(f"LOW CONFIDENCE: {filename} {score:.2f}: {episode.get("title")}")
|
||||
logger.warning(
|
||||
f"LOW CONFIDENCE: {filename} {score:.2f}: {normalize(episode.get('title'))}"
|
||||
)
|
||||
continue
|
||||
|
||||
safe_title = re.sub(
|
||||
r'[<>:"/\\|?*]',
|
||||
'',
|
||||
"",
|
||||
episode["title"],
|
||||
)
|
||||
|
||||
new_name = (
|
||||
f"{media_data.get("name")}"
|
||||
f"{media_data.get('name')}"
|
||||
f" - S{episode['season']:02d}"
|
||||
f"E{episode['episode']:02d}"
|
||||
f" - {safe_title}"
|
||||
@@ -219,12 +255,10 @@ def process_series(series_path: Path, dry_run: bool = False):
|
||||
)
|
||||
|
||||
target = file.with_name(new_name)
|
||||
|
||||
logger.info(
|
||||
f"{file.name}"
|
||||
f" -> {new_name}, "
|
||||
f" score={score:.3f}"
|
||||
)
|
||||
if score < 1:
|
||||
logger.debug(f"{file.name} -> {new_name}, score={score:.3f}")
|
||||
elif score == 1:
|
||||
logger.trace(f"{file.name} -> {new_name}, score={score:.3f}")
|
||||
|
||||
if not dry_run:
|
||||
file.rename(target)
|
||||
@@ -234,7 +268,7 @@ def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"path",
|
||||
help='Either a series folder "Maya the Bee (1975) [10620]" or a root folder containing such folders'
|
||||
help='Either a series folder "Maya the Bee (1975) [10620]" or a root folder containing such folders',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
@@ -268,7 +302,7 @@ def main():
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
)
|
||||
|
||||
input_path = Path(args.path)
|
||||
input_path = Path(args.path).expanduser().resolve()
|
||||
|
||||
if not input_path.is_dir():
|
||||
parser.error(f"Directory not found: {input_path}")
|
||||
@@ -278,14 +312,14 @@ def main():
|
||||
# Input is already a series folder
|
||||
if re.match(r"^(.*?)\s*\((\d{4})\)\s*\[(\d+)]$", input_path.name):
|
||||
logger.info(f"Process series: {input_path.name}")
|
||||
process_series(input_path,dry_run)
|
||||
process_series(input_path, dry_run)
|
||||
|
||||
# Input is a root folder containing series folders
|
||||
# Input is a root folder containing series folders
|
||||
else:
|
||||
for series_path in sorted(input_path.iterdir()):
|
||||
if series_path.is_dir():
|
||||
process_series(series_path,dry_run)
|
||||
process_series(series_path, dry_run)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
Reference in new issue
Block a user