fix illegal-character stripping gluing digits together in final folder/file names
This commit is contained in:
+78
-31
@@ -15,6 +15,7 @@ import shutil
|
|||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import urllib.error
|
import urllib.error
|
||||||
@@ -37,10 +38,18 @@ DEFAULT_SERVICE_INTERVAL = 300
|
|||||||
|
|
||||||
VERBOSE = False
|
VERBOSE = False
|
||||||
|
|
||||||
|
_vprint_local = threading.local()
|
||||||
|
|
||||||
|
|
||||||
def vprint(*args, **kwargs):
|
def vprint(*args, **kwargs):
|
||||||
if VERBOSE:
|
if not VERBOSE:
|
||||||
print(*args, **kwargs)
|
return
|
||||||
|
buffer = getattr(_vprint_local, "buffer", None)
|
||||||
|
if buffer is not None:
|
||||||
|
text = kwargs.get("sep", " ").join(str(a) for a in args)
|
||||||
|
buffer.append(text)
|
||||||
|
return
|
||||||
|
print(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
VIDEO_EXTENSIONS = {
|
VIDEO_EXTENSIONS = {
|
||||||
@@ -167,17 +176,31 @@ SUPERSCRIPT_DIGITS = {
|
|||||||
'⁸': ' 8', '⁹': ' 9',
|
'⁸': ' 8', '⁹': ' 9',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VULGAR_FRACTIONS = {
|
||||||
|
'¼': ' 1/4', '½': ' 1/2', '¾': ' 3/4',
|
||||||
|
'⅓': ' 1/3', '⅔': ' 2/3',
|
||||||
|
'⅕': ' 1/5', '⅖': ' 2/5', '⅗': ' 3/5', '⅘': ' 4/5',
|
||||||
|
'⅙': ' 1/6', '⅚': ' 5/6',
|
||||||
|
'⅐': ' 1/7',
|
||||||
|
'⅛': ' 1/8', '⅜': ' 3/8', '⅝': ' 5/8', '⅞': ' 7/8',
|
||||||
|
'⅑': ' 1/9',
|
||||||
|
'⅒': ' 1/10',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def normalize_special_chars(name):
|
def normalize_special_chars(name):
|
||||||
for special, normal in SUPERSCRIPT_DIGITS.items():
|
for special, normal in SUPERSCRIPT_DIGITS.items():
|
||||||
name = name.replace(special, normal)
|
name = name.replace(special, normal)
|
||||||
|
for special, normal in VULGAR_FRACTIONS.items():
|
||||||
|
name = name.replace(special, normal)
|
||||||
|
name = name.replace('⁄', '/')
|
||||||
decomposed = unicodedata.normalize('NFKD', name)
|
decomposed = unicodedata.normalize('NFKD', name)
|
||||||
return ''.join(c for c in decomposed if not unicodedata.combining(c))
|
return ''.join(c for c in decomposed if not unicodedata.combining(c))
|
||||||
|
|
||||||
|
|
||||||
def clean_name(name):
|
def clean_name(name):
|
||||||
name = normalize_special_chars(name)
|
name = normalize_special_chars(name)
|
||||||
name = re.sub(r'[._-]+', ' ', name)
|
name = re.sub(r'[./-]+', ' ', name)
|
||||||
return re.sub(r'[^A-Za-z0-9 ]', '', name)
|
return re.sub(r'[^A-Za-z0-9 ]', '', name)
|
||||||
|
|
||||||
|
|
||||||
@@ -191,7 +214,7 @@ def clean_and_squeeze(name):
|
|||||||
|
|
||||||
def clean_name_keep_hyphens(name):
|
def clean_name_keep_hyphens(name):
|
||||||
name = normalize_special_chars(name)
|
name = normalize_special_chars(name)
|
||||||
name = re.sub(r'[._]+', ' ', name)
|
name = re.sub(r'[./]+', ' ', name)
|
||||||
return re.sub(r'[^A-Za-z0-9 \-]', '', name)
|
return re.sub(r'[^A-Za-z0-9 \-]', '', name)
|
||||||
|
|
||||||
|
|
||||||
@@ -789,6 +812,9 @@ def result_year(media_type, result):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
YEAR_MATCH_MIN_SIMILARITY = 0.4
|
||||||
|
|
||||||
|
|
||||||
def best_match(media_type, results, year, query=None):
|
def best_match(media_type, results, year, query=None):
|
||||||
if not results:
|
if not results:
|
||||||
return None
|
return None
|
||||||
@@ -804,9 +830,20 @@ def best_match(media_type, results, year, query=None):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
if year:
|
if year:
|
||||||
for r in results:
|
year_matches = [r for r in results if result_year(media_type, r) == year]
|
||||||
if result_year(media_type, r) == year:
|
if not year_matches:
|
||||||
return r
|
return None
|
||||||
|
if not query_norm:
|
||||||
|
return year_matches[0]
|
||||||
|
|
||||||
|
def title_similarity(r):
|
||||||
|
return difflib.SequenceMatcher(
|
||||||
|
None, query_norm, clean_and_squeeze(result_title(media_type, r) or "").lower()
|
||||||
|
).ratio()
|
||||||
|
|
||||||
|
best = max(year_matches, key=title_similarity)
|
||||||
|
if title_similarity(best) >= YEAR_MATCH_MIN_SIMILARITY:
|
||||||
|
return best
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if query_norm:
|
if query_norm:
|
||||||
@@ -1083,6 +1120,7 @@ def detect_resolution(name):
|
|||||||
|
|
||||||
|
|
||||||
def sanitize_rendered_name(name):
|
def sanitize_rendered_name(name):
|
||||||
|
name = name.replace('/', '/').replace('\\', ' ')
|
||||||
name = FILESYSTEM_ILLEGAL_CHARS_PATTERN.sub('', name)
|
name = FILESYSTEM_ILLEGAL_CHARS_PATTERN.sub('', name)
|
||||||
name = EMPTY_GROUP_PATTERN.sub('', name)
|
name = EMPTY_GROUP_PATTERN.sub('', name)
|
||||||
name = squeeze_spaces(name)
|
name = squeeze_spaces(name)
|
||||||
@@ -1230,7 +1268,8 @@ def lookup_folder(api_key, media_type, raw_name, hint_year=None):
|
|||||||
|
|
||||||
if not match and "-" in raw_name:
|
if not match and "-" in raw_name:
|
||||||
hyphen_query = build_query(raw_name, year, keep_hyphens=True)
|
hyphen_query = build_query(raw_name, year, keep_hyphens=True)
|
||||||
if hyphen_query != query:
|
hyphen_query = hyphen_query.strip(" -")
|
||||||
|
if hyphen_query and hyphen_query != query:
|
||||||
quoted_query = f'"{hyphen_query}"'
|
quoted_query = f'"{hyphen_query}"'
|
||||||
vprint(f" No match, retrying as quoted phrase (TMDb treats bare hyphens as query operators): query={quoted_query!r}")
|
vprint(f" No match, retrying as quoted phrase (TMDb treats bare hyphens as query operators): query={quoted_query!r}")
|
||||||
hyphen_results, hyphen_err = tmdb_search(api_key, media_type, quoted_query)
|
hyphen_results, hyphen_err = tmdb_search(api_key, media_type, quoted_query)
|
||||||
@@ -3167,6 +3206,17 @@ def process_loose_tv_files(share, api_key, log, test_mode, test_limit, args):
|
|||||||
LOOKUP_WORKERS = 8
|
LOOKUP_WORKERS = 8
|
||||||
|
|
||||||
|
|
||||||
|
def _buffered_lookup(api_key, media_type, raw_name, hint_year):
|
||||||
|
_vprint_local.buffer = []
|
||||||
|
try:
|
||||||
|
result = lookup_folder(api_key, media_type, raw_name, hint_year)
|
||||||
|
except Exception as e:
|
||||||
|
result = (None, None, None, f"Lookup failed: {raw_name} ({e})")
|
||||||
|
buffer = _vprint_local.buffer
|
||||||
|
_vprint_local.buffer = None
|
||||||
|
return result, buffer
|
||||||
|
|
||||||
|
|
||||||
def prefetch_lookups(api_key, media_type, folders):
|
def prefetch_lookups(api_key, media_type, folders):
|
||||||
lookup_cache = {}
|
lookup_cache = {}
|
||||||
if not folders:
|
if not folders:
|
||||||
@@ -3174,30 +3224,27 @@ def prefetch_lookups(api_key, media_type, folders):
|
|||||||
|
|
||||||
print(f"Looking up {len(folders)} folder(s) on TMDb...")
|
print(f"Looking up {len(folders)} folder(s) on TMDb...")
|
||||||
|
|
||||||
global vprint
|
with concurrent.futures.ThreadPoolExecutor(max_workers=LOOKUP_WORKERS) as executor:
|
||||||
real_vprint = vprint
|
futures = {}
|
||||||
if not VERBOSE:
|
for folder in folders:
|
||||||
vprint = lambda *a, **k: None
|
raw_name = folder.name
|
||||||
|
hint_year = None
|
||||||
|
if extract_year(raw_name) is None:
|
||||||
|
hint_year = infer_year_from_files(folder)
|
||||||
|
futures[executor.submit(_buffered_lookup, api_key, media_type, raw_name, hint_year)] = (folder, hint_year)
|
||||||
|
|
||||||
try:
|
for future in concurrent.futures.as_completed(futures):
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=LOOKUP_WORKERS) as executor:
|
folder, hint_year = futures[future]
|
||||||
futures = {}
|
try:
|
||||||
for folder in folders:
|
result, buffer = future.result()
|
||||||
raw_name = folder.name
|
except Exception as e:
|
||||||
hint_year = None
|
result = (None, None, None, f"Lookup failed: {folder.name} ({e})")
|
||||||
if extract_year(raw_name) is None:
|
buffer = []
|
||||||
hint_year = infer_year_from_files(folder)
|
if VERBOSE and buffer:
|
||||||
futures[executor.submit(lookup_folder, api_key, media_type, raw_name, hint_year)] = (folder, hint_year)
|
print(f"-- {folder.name} --")
|
||||||
|
for line in buffer:
|
||||||
for future in concurrent.futures.as_completed(futures):
|
print(line)
|
||||||
folder, hint_year = futures[future]
|
lookup_cache[folder] = (hint_year, result)
|
||||||
try:
|
|
||||||
result = future.result()
|
|
||||||
except Exception as e:
|
|
||||||
result = (None, None, None, f"Lookup failed: {folder.name} ({e})")
|
|
||||||
lookup_cache[folder] = (hint_year, result)
|
|
||||||
finally:
|
|
||||||
vprint = real_vprint
|
|
||||||
|
|
||||||
print()
|
print()
|
||||||
return lookup_cache
|
return lookup_cache
|
||||||
|
|||||||
Reference in New Issue
Block a user