Skip to content

Commit

Permalink
async new name templating
Browse files Browse the repository at this point in the history
  • Loading branch information
NextFire committed Jan 29, 2024
1 parent 78d242e commit f4397cc
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 75 deletions.
38 changes: 29 additions & 9 deletions sachi/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import asyncio
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Self, assert_never, cast
from typing import Callable, Self, assert_never, cast

import jinja2
from guessit import guessit
from pymediainfo import MediaInfo
from textual.widgets.data_table import RowKey

from sachi.config import BaseConfig, read_config
from sachi.context import FileBotContext
Expand All @@ -21,19 +20,24 @@
@dataclass
class SachiMatch:
parent: SachiParentModel
episode: SachiEpisodeModel | None
episode: SachiEpisodeModel


class SachiFile:
def __init__(self, path: Path):
def __init__(
self, path: Path, base_dir: Path, set_rename_cell: Callable[[str | None], None]
):
self.path = path
self.base_dir = base_dir
self.set_rename_cell = set_rename_cell

self._match: SachiMatch | None = None

self.ctx = FileBotContext()
self.analyze_filename()
self.media_analysis_done = asyncio.Event()

self.row_key: RowKey | None = None
self.new_path = asyncio.Future[Path]()

@property
def match(self) -> SachiMatch | None:
Expand All @@ -42,10 +46,24 @@ def match(self) -> SachiMatch | None:
@match.setter
def match(self, value: SachiMatch | None):
self._match = value

self.set_rename_cell(
f"{value.parent.title} ({value.parent.year}) "
f"- {value.episode.season:02}x{value.episode.episode:02} "
f"- {value.episode.name}"
if value
else None
)

self.analyze_match()
if not self.media_analysis_done.is_set():

if value is not None and not self.media_analysis_done.is_set():
asyncio.create_task(asyncio.to_thread(self.analyze_media))

if self.new_path.done():
self.new_path = asyncio.Future[Path]()
asyncio.create_task(self.template_new_path())

def analyze_filename(self):
guess = cast(dict, guessit(self.path.name))
self.ctx.source = guess.get("source", None)
Expand Down Expand Up @@ -77,9 +95,9 @@ def analyze_match(self):
self.ctx.s00e00 = f"S{episode.season:02}E{episode.episode:02}"
self.ctx.t = episode.name

async def new_path(self, base: Path) -> Path | None:
async def template_new_path(self):
if self.match is None:
return None
return

await self.media_analysis_done.wait()

Expand All @@ -99,7 +117,9 @@ async def new_path(self, base: Path) -> Path | None:
new_segment = template.render(asdict(self.ctx))
new_segment = FS_SPECIAL_CHARS.sub("", new_segment)
new_segment = new_segment.replace(FAKE_SLASH, "/")
return (base / new_segment).with_suffix(self.path.suffix)
new_path = (self.base_dir / new_segment).with_suffix(self.path.suffix)
self.set_rename_cell(str(new_path.relative_to(self.base_dir)))
self.new_path.set_result(new_path)

def __eq__(self, other: object):
return isinstance(other, SachiFile) and self.path == other.path
Expand Down
27 changes: 16 additions & 11 deletions sachi/screens/episodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

from textual import on, work
from textual.app import ComposeResult
from textual.containers import Container
from textual.containers import Horizontal, Vertical
from textual.reactive import reactive
from textual.screen import ModalScreen, Screen
from textual.widgets import (
DataTable,
Footer,
Header,
Input,
Expand Down Expand Up @@ -58,13 +59,14 @@ def selected_episodes(self) -> list[SachiEpisodeModel]:

def compose(self) -> ComposeResult:
yield Header()
with Container(id="search-header", classes="my-1"):
yield Input(placeholder="Search", id="search-input", restrict=r".+")
yield Select(
((f"{s.service} ({s.media_type})", s) for s in SOURCE_CLASSES),
prompt="Source",
)
yield SelectionList[int](classes="my-1")
with Vertical():
with Horizontal():
yield Input(placeholder="Search", id="search-input", restrict=r".+")
yield Select(
((f"{s.service} ({s.media_type})", s) for s in SOURCE_CLASSES),
prompt="Source",
)
yield SelectionList[int]()
yield Footer()

# Methods
Expand Down Expand Up @@ -112,10 +114,12 @@ def action_append_selection(self):
return
parent = self.sachi_parent
screen = cast(RenameScreen, self.app.get_screen("rename"))
table = screen.query_one(DataTable)
j = 0
for file in screen.files:
for row in table.ordered_rows:
if j >= len(self.selected_episodes):
break
file = screen.files[row.key]
if file.match is None:
file.match = SachiMatch(
parent=parent, episode=self.selected_episodes[j]
Expand All @@ -129,7 +133,8 @@ def action_replace_selection(self):
return
parent = self.sachi_parent
screen = cast(RenameScreen, self.app.get_screen("rename"))
for file, ep in zip(screen.files, self.selected_episodes):
file.match = SachiMatch(parent=parent, episode=ep)
table = screen.query_one(DataTable)
for row, ep in zip(table.ordered_rows, self.selected_episodes):
screen.files[row.key].match = SachiMatch(parent=parent, episode=ep)
self.app.switch_screen("rename")
self.deselect_all()
30 changes: 16 additions & 14 deletions sachi/screens/episodes.tcss
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
Horizontal {
height: auto;

Input {
width: 3fr;
}

Select {
width: 1fr;
}
}

SelectionList {
height: 100%;
}

ModalScreen {
align: center middle;
}
Expand All @@ -11,17 +27,3 @@ ListView {
Label {
padding: 1 2;
}

.my-1 {
margin: 1 0;
}

#search-header {
layout: grid;
grid-size: 4 1;
height: auto;
}

#search-input {
column-span: 3;
}
75 changes: 34 additions & 41 deletions sachi/screens/rename.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from functools import partial
from pathlib import Path
from typing import Generator

from textual.app import ComposeResult
from textual.reactive import reactive
from textual.screen import Screen
from textual.widgets import DataTable, Footer, Header
from textual.widgets.data_table import RowKey

from sachi.models import SachiFile

Expand All @@ -16,80 +19,70 @@ class RenameScreen(Screen):
("p", "apply_renames", "Apply"),
]

files: reactive[list[SachiFile]] = reactive([])
files: reactive[dict[RowKey, SachiFile]] = reactive({})

def __init__(self, file_or_dir: Path, **kwargs):
super().__init__(**kwargs)
self.load(file_or_dir)
self.files.sort()
self.file_or_dir = file_or_dir
self.base_dir = file_or_dir.parent if file_or_dir.is_file() else file_or_dir

def compose(self) -> ComposeResult:
yield Header()
table = DataTable(fixed_rows=1, zebra_stripes=True)
table.add_columns("From", "To")
yield table
yield DataTable(fixed_rows=1, zebra_stripes=True)
yield Footer()

# Methods

def load(self, file_or_dir: Path):
def iter_files(self, file_or_dir: Path) -> Generator[Path, None, None]:
if file_or_dir.name.startswith("."):
return
if file_or_dir.is_file():
self.files.append(SachiFile(path=file_or_dir))
yield file_or_dir
elif file_or_dir.is_dir():
for file in file_or_dir.iterdir():
self.load(file)
yield from self.iter_files(file)
else:
raise RuntimeError(f"Invalid file or directory: {file_or_dir}")

# Event handlers

async def on_mount(self):
table = self.query_one(DataTable)
for file in self.files:
new_path = await file.new_path(self.base_dir)
file.row_key = table.add_row(
str(file.path.relative_to(self.base_dir)),
str(new_path.relative_to(self.base_dir)) if new_path else None,
col_keys = table.add_columns("From", "To")
for path in self.iter_files(self.file_or_dir):
row_key = table.add_row(
str(path.relative_to(self.base_dir)),
None,
)
table.focus()

async def on_screen_resume(self):
table = self.query_one(DataTable)
table.clear()
for file in self.files:
new_path = await file.new_path(self.base_dir)
file.row_key = table.add_row(
str(file.path.relative_to(self.base_dir)),
str(new_path.relative_to(self.base_dir)) if new_path else None,
self.files[row_key] = SachiFile(
path,
self.base_dir,
partial(table.update_cell, row_key, col_keys[1], update_width=True),
)
table.sort(col_keys[0])
table.focus()

# Key bindings

def action_remove_element(self):
table = self.query_one(DataTable)
cood = table.cursor_coordinate
keys = table.coordinate_to_cell_key(cood)
if cood.column == 0:
del self.files[cood.row]
table.remove_row(keys.row_key)
if cood.column == 1:
self.files[cood.row].match = None
table.update_cell_at(cood, None)
cell_key = table.coordinate_to_cell_key(table.cursor_coordinate)
col_i = table.cursor_column
if col_i == 0:
table.remove_row(cell_key.row_key)
del self.files[cell_key.row_key]
elif col_i == 1:
self.files[cell_key.row_key].match = None

async def action_apply_renames(self):
table = self.query_one(DataTable)
renamed = []
for i, file in enumerate(self.files):
new_path = await file.new_path(self.base_dir)
if new_path is not None:
for row in table.ordered_rows:
file = self.files[row.key]

if file.match is not None:
new_path = await file.new_path
new_path.parent.mkdir(parents=True, exist_ok=True)
file.path.rename(new_path)
renamed.append(i)
if file.row_key is not None:
table.remove_row(file.row_key)
for i in reversed(renamed):
del self.files[i]

table.remove_row(row.key)
del self.files[row.key]

0 comments on commit f4397cc

Please sign in to comment.