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
+14
-11
@@ -1,11 +1,14 @@
|
||||
from .core import safe_rename
|
||||
from .core import safe_delete
|
||||
from .core import get_audio_tracks
|
||||
from .core import get_subtitle_tracks
|
||||
from .core import words
|
||||
from .core import word_similarity
|
||||
from .core import score_match_jaccard
|
||||
from .core import score_match_fuzzy
|
||||
from .core import combined_score
|
||||
from .core import tmdb_get
|
||||
from .core import tvdb_get
|
||||
from .core import (
|
||||
combined_score,
|
||||
get_audio_tracks,
|
||||
get_subtitle_tracks,
|
||||
safe_delete,
|
||||
safe_rename,
|
||||
score_match_fuzzy,
|
||||
score_match_jaccard,
|
||||
score_match_recall_jaccard,
|
||||
tmdb_get,
|
||||
tvdb_get,
|
||||
word_similarity,
|
||||
words,
|
||||
)
|
||||
+91
-61
@@ -1,11 +1,11 @@
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
import requests
|
||||
import shutil
|
||||
import subprocess
|
||||
from difflib import SequenceMatcher
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
# -----------------------------
|
||||
@@ -17,19 +17,26 @@ def safe_delete(path: Path):
|
||||
else:
|
||||
path.unlink()
|
||||
|
||||
def safe_rename(src: Path, dst: Path, dry: bool=False, confirm: bool=False,
|
||||
silent=False,overwrite=False) -> (str, bool):
|
||||
|
||||
def safe_rename(
|
||||
src: Path,
|
||||
dst: Path,
|
||||
dry: bool = False,
|
||||
confirm: bool = False,
|
||||
silent=False,
|
||||
overwrite=False,
|
||||
) -> tuple[str, bool]:
|
||||
"""
|
||||
Safely rename a file or directory without overwriting existing files.
|
||||
Automatically appends _1, _2, ... if needed.
|
||||
"""
|
||||
Safely rename a file or directory without overwriting existing files.
|
||||
Automatically appends _1, _2, ... if needed.
|
||||
"""
|
||||
|
||||
src = Path(src)
|
||||
dst = Path(dst)
|
||||
|
||||
# nothing to do
|
||||
if src.resolve() == dst.resolve():
|
||||
return
|
||||
return "", False
|
||||
|
||||
directory = dst.parent
|
||||
base = dst.stem
|
||||
@@ -42,7 +49,7 @@ def safe_rename(src: Path, dst: Path, dry: bool=False, confirm: bool=False,
|
||||
while final_dst.exists():
|
||||
final_dst = directory / f"{base}_{counter}{ext}"
|
||||
counter += 1
|
||||
ret=f"{src} → {dst}"
|
||||
ret = f"{src} → {dst}"
|
||||
if not silent:
|
||||
print(ret)
|
||||
|
||||
@@ -56,11 +63,14 @@ def safe_rename(src: Path, dst: Path, dry: bool=False, confirm: bool=False,
|
||||
|
||||
if dst.exists() and not overwrite:
|
||||
print("File already exists...")
|
||||
return "",False
|
||||
return "", False
|
||||
src.rename(dst)
|
||||
return ret, True
|
||||
|
||||
def get_audio_tracks( source: str, title: int | None = None, handbrake: bool = False) -> list[dict]:
|
||||
|
||||
def get_audio_tracks(
|
||||
source: str, title: int | None = None, handbrake: bool = False
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Return audio track information from a media file or DVD title.
|
||||
|
||||
@@ -103,8 +113,10 @@ def get_audio_tracks( source: str, title: int | None = None, handbrake: bool = F
|
||||
result = subprocess.run(
|
||||
[
|
||||
"HandBrakeCLI",
|
||||
"-i", source,
|
||||
"-t", str(title),
|
||||
"-i",
|
||||
source,
|
||||
"-t",
|
||||
str(title),
|
||||
"--scan",
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -113,22 +125,24 @@ def get_audio_tracks( source: str, title: int | None = None, handbrake: bool = F
|
||||
check=True,
|
||||
)
|
||||
|
||||
pattern = re.compile(
|
||||
r"\+\s+(\d+),\s+(.+?)\s+\((?:AC3|AAC|MP3|E-AC-3|DTS)"
|
||||
)
|
||||
pattern = re.compile(r"\+\s+(\d+),\s+(.+?)\s+\((?:AC3|AAC|MP3|E-AC-3|DTS)")
|
||||
|
||||
for match in pattern.finditer(result.stdout):
|
||||
tracks.append({
|
||||
"number": int(match.group(1)),
|
||||
"description": match.group(2).strip(),
|
||||
})
|
||||
tracks.append(
|
||||
{
|
||||
"number": int(match.group(1)),
|
||||
"description": match.group(2).strip(),
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ffprobe",
|
||||
"-v", "quiet",
|
||||
"-print_format", "json",
|
||||
"-v",
|
||||
"quiet",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_streams",
|
||||
source,
|
||||
],
|
||||
@@ -143,31 +157,33 @@ def get_audio_tracks( source: str, title: int | None = None, handbrake: bool = F
|
||||
if stream.get("codec_type") != "audio":
|
||||
continue
|
||||
|
||||
tracks.append({
|
||||
"number": stream["index"],
|
||||
"description": (
|
||||
stream.get("tags", {}).get("title")
|
||||
or stream.get("tags", {}).get("language")
|
||||
or f"Track {stream['index']}"
|
||||
),
|
||||
"primary": stream.get("codec_type") == "audio" and stream.get("disposition", {}).get("default") == 1
|
||||
})
|
||||
tracks.append(
|
||||
{
|
||||
"number": stream["index"],
|
||||
"description": (
|
||||
stream.get("tags", {}).get("title")
|
||||
or stream.get("tags", {}).get("language")
|
||||
or f"Track {stream['index']}"
|
||||
),
|
||||
"primary": stream.get("codec_type") == "audio"
|
||||
and stream.get("disposition", {}).get("default") == 1,
|
||||
}
|
||||
)
|
||||
|
||||
if not tracks:
|
||||
tracks.append({
|
||||
"number": 0,
|
||||
"description": "",
|
||||
"primary": False
|
||||
})
|
||||
tracks.append({"number": 0, "description": "", "primary": False})
|
||||
|
||||
return tracks
|
||||
|
||||
|
||||
def get_subtitle_tracks(source):
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ffprobe",
|
||||
"-v", "quiet",
|
||||
"-print_format", "json",
|
||||
"-v",
|
||||
"quiet",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_streams",
|
||||
source,
|
||||
],
|
||||
@@ -184,36 +200,48 @@ def get_subtitle_tracks(source):
|
||||
if stream.get("codec_type") != "subtitle":
|
||||
continue
|
||||
|
||||
tracks.append({
|
||||
"number": stream["index"],
|
||||
"description": (
|
||||
stream.get("tags", {}).get("title")
|
||||
or stream.get("tags", {}).get("language")
|
||||
or f"Subtitle {stream['index']}"
|
||||
),
|
||||
"primary": (
|
||||
stream.get("disposition", {}).get("default") == 1
|
||||
),
|
||||
})
|
||||
tracks.append(
|
||||
{
|
||||
"number": stream["index"],
|
||||
"description": (
|
||||
stream.get("tags", {}).get("title")
|
||||
or stream.get("tags", {}).get("language")
|
||||
or f"Subtitle {stream['index']}"
|
||||
),
|
||||
"primary": (stream.get("disposition", {}).get("default") == 1),
|
||||
}
|
||||
)
|
||||
|
||||
if not tracks:
|
||||
tracks.append({
|
||||
"number": 0,
|
||||
"description": "",
|
||||
"primary": False,
|
||||
})
|
||||
tracks.append(
|
||||
{
|
||||
"number": 0,
|
||||
"description": "",
|
||||
"primary": False,
|
||||
}
|
||||
)
|
||||
|
||||
return tracks
|
||||
|
||||
|
||||
def words(text):
|
||||
return set(filter(None, re.split(r"[^a-z0-9]+", text.lower())))
|
||||
return set(re.findall(r"[^\W_]+", text.lower()))
|
||||
|
||||
|
||||
def word_similarity(a: str, b: str) -> float:
|
||||
return SequenceMatcher(None, a, b).ratio()
|
||||
|
||||
|
||||
def score_match_recall_jaccard(
|
||||
search_words: set[str], candidate_words: set[str]
|
||||
) -> float:
|
||||
if not search_words:
|
||||
return 0.0
|
||||
|
||||
intersection = search_words & candidate_words
|
||||
return len(intersection) / len(search_words)
|
||||
|
||||
|
||||
def score_match_jaccard(search_words, candidate_words):
|
||||
intersection = search_words & candidate_words
|
||||
union = search_words | candidate_words
|
||||
@@ -239,8 +267,9 @@ def score_match_fuzzy(search_words, candidate_words):
|
||||
return total / len(search_words)
|
||||
|
||||
|
||||
def combined_score(score_fuzzy: float, score_jaccard: float,
|
||||
weight_fuzzy: float, weight_jaccard: float) -> float:
|
||||
def combined_score(
|
||||
score_fuzzy: float, score_jaccard: float, weight_fuzzy: float, weight_jaccard: float
|
||||
) -> float:
|
||||
return score_fuzzy * weight_fuzzy + score_jaccard * weight_jaccard
|
||||
|
||||
|
||||
@@ -278,6 +307,7 @@ def tmdb_get(endpoint, params=None):
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def tvdb_get(endpoint, params=None):
|
||||
"""Perform a GET request against TVDB."""
|
||||
BASE_URL = "https://api4.thetvdb.com/v4"
|
||||
@@ -293,4 +323,4 @@ def tvdb_get(endpoint, params=None):
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
return response.json()
|
||||
Reference in new issue
Block a user