snapshot
This commit is contained in:
+87
-58
@@ -10,24 +10,25 @@ import typing
|
||||
from pathlib import Path
|
||||
|
||||
script_path = Path(__file__)
|
||||
db_filename = script_path.with_name(f'{script_path.stem}.known.txt')
|
||||
db_filename = script_path.with_name(f"{script_path.stem}.known.txt")
|
||||
|
||||
|
||||
def save_actors(actors: typing.Iterable[str]):
|
||||
existing = load_actors()
|
||||
all_actors = sorted({*existing, *actors})
|
||||
with db_filename.open('w', encoding='utf-8', newline='\n') as f:
|
||||
with db_filename.open("w", encoding="utf-8", newline="\n") as f:
|
||||
f.writelines(all_actors)
|
||||
|
||||
|
||||
def load_actors() -> set[str]:
|
||||
try:
|
||||
with db_filename.open('w', encoding='utf-8', newline='\n') as f:
|
||||
with db_filename.open(encoding="utf-8", newline="\n") as f:
|
||||
return {line.strip() for line in f}
|
||||
except FileNotFoundError:
|
||||
return set()
|
||||
|
||||
|
||||
re_date_only = re.compile(r"\b(\d{4})\b") # 2024
|
||||
re_date_us = re.compile(r"\b(\d{2})\D(\d{2})\D(\d{2})\b") # 12/31/21
|
||||
re_date_iso = re.compile(r"\b(\d{4})\D?(\d{2})\D?(\d{2})\b") # 2020-12-31
|
||||
re_date_iso_short = re.compile(r"(\d{2})\D?(\d{2})\D?(\d{2})") # 20-12-31"'
|
||||
@@ -53,10 +54,19 @@ class ParsedDate:
|
||||
|
||||
|
||||
def parse_date(filename: str) -> ParsedDate | None:
|
||||
def from_date_only():
|
||||
match = re_date_only.search(filename)
|
||||
y = int(match.group(1))
|
||||
return ParsedDate(
|
||||
datetime.date(y, 1, 1), start=match.start(), end=match.end()
|
||||
)
|
||||
|
||||
def from_us_format():
|
||||
match = re_date_us.search(filename)
|
||||
m, d, y = map(int, match.groups())
|
||||
return ParsedDate(datetime.date(y + 2000, m, d), start=match.start(), end=match.end())
|
||||
return ParsedDate(
|
||||
datetime.date(y + 2000, m, d), start=match.start(), end=match.end()
|
||||
)
|
||||
|
||||
def from_iso():
|
||||
match = re_date_iso.search(filename)
|
||||
@@ -66,7 +76,9 @@ def parse_date(filename: str) -> ParsedDate | None:
|
||||
def from_iso_short():
|
||||
match = re_date_iso_short.search(filename)
|
||||
y, m, d = map(int, match.groups())
|
||||
return ParsedDate(datetime.date(y + 2000, m, d), start=match.start(), end=match.end())
|
||||
return ParsedDate(
|
||||
datetime.date(y + 2000, m, d), start=match.start(), end=match.end()
|
||||
)
|
||||
|
||||
def from_iso_reversed():
|
||||
match = re_date_iso_rev.search(filename)
|
||||
@@ -76,10 +88,13 @@ def parse_date(filename: str) -> ParsedDate | None:
|
||||
def from_iso_reversed_short():
|
||||
match = re_date_iso_rev_short.search(filename)
|
||||
d, m, y = map(int, match.groups())
|
||||
return ParsedDate(datetime.date(y + 2000, m, d), start=match.start(), end=match.end())
|
||||
return ParsedDate(
|
||||
datetime.date(y + 2000, m, d), start=match.start(), end=match.end()
|
||||
)
|
||||
|
||||
candidates = []
|
||||
for fn in [
|
||||
from_date_only,
|
||||
from_us_format,
|
||||
from_iso,
|
||||
from_iso_short,
|
||||
@@ -94,7 +109,9 @@ def parse_date(filename: str) -> ParsedDate | None:
|
||||
today = datetime.date.today()
|
||||
future_threshold = today + datetime.timedelta(days=60)
|
||||
past_threshold = datetime.date(2010, 1, 1)
|
||||
candidates = [it for it in candidates if past_threshold < it.date < future_threshold]
|
||||
candidates = [
|
||||
it for it in candidates if past_threshold < it.date < future_threshold
|
||||
]
|
||||
|
||||
if not candidates:
|
||||
return
|
||||
@@ -112,31 +129,35 @@ class Release:
|
||||
def to_filename(self):
|
||||
parts = []
|
||||
if self.actors:
|
||||
parts.append(', '.join(self.actors))
|
||||
parts.append(", ".join(self.actors))
|
||||
if self.studio:
|
||||
parts.append(f'@{self.studio}')
|
||||
parts.append(f"@{self.studio}")
|
||||
if self.title:
|
||||
parts.append(self.title)
|
||||
if self.released_at:
|
||||
parts.append(self.released_at.isoformat())
|
||||
return ' -- '.join(parts)
|
||||
return " -- ".join(parts)
|
||||
|
||||
|
||||
def trash(path: Path):
|
||||
filename = f'"{path}"'
|
||||
cmd = ['osascript', '-e', f'tell app "Finder" to move (POSIX file {filename}) to trash']
|
||||
cmd = [
|
||||
"osascript",
|
||||
"-e",
|
||||
f'tell app "Finder" to move (POSIX file {filename}) to trash',
|
||||
]
|
||||
subprocess.run(cmd).check_returncode()
|
||||
|
||||
|
||||
def filenames_to_actors():
|
||||
sources = [
|
||||
Path(r'/Users/abdus/Downloads/temp/'),
|
||||
Path(r'/Volumes/BANDAID/_temp/__reenc/'),
|
||||
Path(r'/Volumes/BANDAID/_temp/'),
|
||||
Path(r"/Users/abdus/Downloads/temp/"),
|
||||
Path(r"/Volumes/BANDAID/_temp/__reenc/"),
|
||||
Path(r"/Volumes/BANDAID/_temp/"),
|
||||
]
|
||||
actors = set()
|
||||
for it in sources:
|
||||
for f in it.glob('*.mp4'):
|
||||
for f in it.glob("*.mp4"):
|
||||
if not f.is_file():
|
||||
continue
|
||||
if r := parse_release(f.name):
|
||||
@@ -144,78 +165,83 @@ def filenames_to_actors():
|
||||
return actors
|
||||
|
||||
|
||||
def parse_release(filename: str, known_actors: set[str] | None = None):
|
||||
def parse_release(
|
||||
filename: str, known_actors: set[str] | None = None
|
||||
) -> Release | None:
|
||||
if not known_actors:
|
||||
known_actors = set()
|
||||
filename = re.sub(r'\.\w{3,4}$', '', filename)
|
||||
if any(filename.lower().endswith(ext) for ext in [".mp4", ".mkv"]):
|
||||
filename = re.sub(r"\.\w{3,4}$", "", filename)
|
||||
|
||||
def from_own():
|
||||
remaining = re.sub(r'\s+\[[^]]+]$', '', filename)
|
||||
remaining = re.sub(r'\s+\[([^]]+|\d+\w)(,\s*[^]]+)?]', '', remaining)
|
||||
remaining = re.sub(r'\s*--\s*', ' -- ', remaining)
|
||||
remaining = re.sub(r"\s+\[[^]]+]$", "", filename)
|
||||
remaining = re.sub(r"\s+\[([^]]+|\d+\w)(,\s*[^]]+)?]", "", remaining)
|
||||
remaining = re.sub(r"\s*--\s*", " -- ", remaining)
|
||||
|
||||
match remaining.split(' -- '):
|
||||
case [actors, studio, title, date] if studio.startswith('@'):
|
||||
match remaining.split(" -- "):
|
||||
case [actors, studio, title, date] if studio.startswith("@"):
|
||||
return Release(
|
||||
actors=sorted(actors.split(', ')),
|
||||
studio=studio.removeprefix('@'),
|
||||
actors=sorted(actors.split(", ")),
|
||||
studio=studio.removeprefix("@"),
|
||||
title=title,
|
||||
released_at=datetime.date.fromisoformat(date),
|
||||
)
|
||||
case [actors, studio, date] if studio.startswith('@') and (parsed := parse_date(date)):
|
||||
case [actors, studio, date] if studio.startswith("@") and (
|
||||
parsed := parse_date(date)
|
||||
):
|
||||
return Release(
|
||||
actors=sorted(actors.split(', ')),
|
||||
studio=studio.removeprefix('@'),
|
||||
actors=sorted(actors.split(", ")),
|
||||
studio=studio.removeprefix("@"),
|
||||
title=None,
|
||||
released_at=parsed.date,
|
||||
)
|
||||
case [actors, studio, title] if studio.startswith('@'):
|
||||
case [actors, studio, title] if studio.startswith("@"):
|
||||
return Release(
|
||||
actors=sorted(actors.split(', ')),
|
||||
studio=studio.removeprefix('@'),
|
||||
actors=sorted(actors.split(", ")),
|
||||
studio=studio.removeprefix("@"),
|
||||
title=title,
|
||||
released_at=None,
|
||||
)
|
||||
case [actors, title, date] if (parsed := parse_date(date)):
|
||||
return Release(
|
||||
actors=sorted(actors.split(', ')),
|
||||
actors=sorted(actors.split(", ")),
|
||||
studio=None,
|
||||
title=title,
|
||||
released_at=parsed.date,
|
||||
)
|
||||
case [actors, studio] if studio.startswith('@'):
|
||||
case [actors, studio] if studio.startswith("@"):
|
||||
return Release(
|
||||
actors=sorted(actors.split(', ')),
|
||||
actors=sorted(actors.split(", ")),
|
||||
studio=studio,
|
||||
)
|
||||
case [actors, title]:
|
||||
return Release(
|
||||
actors=sorted(actors.split(', ')),
|
||||
actors=sorted(actors.split(", ")),
|
||||
title=title,
|
||||
)
|
||||
|
||||
def from_prt():
|
||||
assert '.PRT' in filename
|
||||
remaining = re.sub(r'\.(720p|1080p|HEVC|x265|PRT|XXX)', ' ', filename)
|
||||
studio = remaining[:remaining.index('.')]
|
||||
# assert '.PRT' in filename
|
||||
remaining = re.sub(r"\.(720p|1080p|HEVC|x265|PRT|XXX)", " ", filename)
|
||||
studio = remaining[: remaining.index(".")]
|
||||
parsed_date = parse_date(remaining)
|
||||
remaining = remaining[parsed_date.end:]
|
||||
remaining = re.sub(r'[. ]+', ' ', remaining).strip()
|
||||
remaining = remaining[parsed_date.end :]
|
||||
remaining = re.sub(r"[. ]+", " ", remaining).strip()
|
||||
|
||||
actors = []
|
||||
title = None
|
||||
|
||||
match remaining.split(' '):
|
||||
case [a_first, a_last, 'And', b_first, b_last]:
|
||||
actors = [f'{a_first} {a_last}', f'{b_first} {b_last}']
|
||||
case [a_first, a_last, 'And', b_first, b_last, *rest]:
|
||||
actors = [f'{a_first} {a_last}', f'{b_first} {b_last}']
|
||||
title = ' '.join(rest)
|
||||
match remaining.split(" "):
|
||||
case [a_first, a_last, "And", b_first, b_last]:
|
||||
actors = [f"{a_first} {a_last}", f"{b_first} {b_last}"]
|
||||
case [a_first, a_last, "And", b_first, b_last, *rest]:
|
||||
actors = [f"{a_first} {a_last}", f"{b_first} {b_last}"]
|
||||
title = " ".join(rest)
|
||||
case [first, last]:
|
||||
actors = [f'{first} {last}']
|
||||
actors = [f"{first} {last}"]
|
||||
case [first, last, *rest]:
|
||||
actors = [f'{first} {last}']
|
||||
title = ' '.join(rest)
|
||||
actors = [f"{first} {last}"]
|
||||
title = " ".join(rest)
|
||||
case [first]:
|
||||
actors = [first]
|
||||
|
||||
@@ -243,7 +269,9 @@ def parse_release(filename: str, known_actors: set[str] | None = None):
|
||||
|
||||
def parse_args():
|
||||
arger = argparse.ArgumentParser()
|
||||
arger.add_argument('filenames', nargs='+', type=lambda v: Path(v).resolve(), help='Filenames')
|
||||
arger.add_argument(
|
||||
"filenames", nargs="+", type=lambda v: Path(v).resolve(), help="Filenames"
|
||||
)
|
||||
return arger.parse_args()
|
||||
|
||||
|
||||
@@ -254,17 +282,18 @@ class MoveOp:
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
'source': str(self.source),
|
||||
'target': str(self.target),
|
||||
"source": str(self.source),
|
||||
"target": str(self.target),
|
||||
}
|
||||
|
||||
|
||||
def write_undo_script(save_dir: Path, ops: list[MoveOp]):
|
||||
now = datetime.datetime.now().isoformat().replace(':', '')
|
||||
save_path = save_dir / f'undo_{now}.json'
|
||||
with save_path.open('w', encoding='utf-8') as f:
|
||||
now = datetime.datetime.now().isoformat().replace(":", "")
|
||||
save_path = save_dir / f"undo_{now}.json"
|
||||
with save_path.open("w", encoding="utf-8") as f:
|
||||
for it in ops:
|
||||
f.write(json.dumps(it.to_dict()) + '\n')
|
||||
f.write(json.dumps(it.to_dict()) + "\n")
|
||||
|
||||
|
||||
def fix_filename():
|
||||
pass
|
||||
@@ -279,18 +308,18 @@ def main():
|
||||
jobs = []
|
||||
for it in paths:
|
||||
if parsed := parse_release(it.name, known_actors=actors):
|
||||
logging.debug(f'filename={it} release={parsed}')
|
||||
logging.debug(f"filename={it} release={parsed}")
|
||||
suggested = parsed.to_filename()
|
||||
target = it.with_stem(suggested)
|
||||
if target.is_file():
|
||||
logging.error('target already exists')
|
||||
logging.error("target already exists")
|
||||
continue
|
||||
jobs.append(MoveOp(source=it, target=target))
|
||||
|
||||
if not jobs:
|
||||
return
|
||||
|
||||
write_undo_script(paths[0].parent, jobs)
|
||||
# write_undo_script(paths[0].parent, jobs)
|
||||
|
||||
for it in jobs:
|
||||
it.target.hardlink_to(it.source)
|
||||
|
||||
Reference in New Issue
Block a user