diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..09af46b --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,23 @@ +name: Upload Python Package +on: + push: + tags: + - '[0-9]+\.[0-9]+\.[0-9]+' + - '[0-9]+\.[0-9]+\.[0-9]+-?rc\.?[0-9]+' + +jobs: + pypi: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Replace media paths in README.md + run: sed -E 's#(\]\((asset/[a-zA-Z0-9._-]+))#](https://github.com/CZ-NIC/mininterface/blob/main/\2?raw=True#g' README.md | less > README.md.tmp && mv README.md.tmp README.md + - name: Build the package + run: python3 -m pip install --upgrade build && python3 -m build + - name: Publish package + uses: pypa/gh-action-pypi-publish@v1.8.10 + with: + password: ${{ secrets.PYPI_GITHUB_TOUCHTIMESTAMP }} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cc7f4eb --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +export TAG := `grep version pyproject.toml | pz --search '"(\d+\.\d+\.\d+(?:rc\d+))?"'` + +release: + git tag $(TAG) + git push origin $(TAG) \ No newline at end of file diff --git a/README.md b/README.md index 143f82a..d849b4f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,80 @@ -XXX asi z toho můžu udělat package na pypi +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -Krusader user action command -`python3 ...gui-touch/gui-touch.py %aList("Selected")%` +Change file timestamps with a dialog window. + +![Gui window](asset/mininterface-gui.avif "Graphical interface") + +GUI automatically fallback to a text interface when display is not available. + +![Text interface](asset/textual.avif "Runs in the terminal") + + +# Installation + +Install with a single command from [PyPi](https://pypi.org/project/touch-timestamp/). + +```bash +pip install touch-timestamp +``` + +Or eventually add [eel](https://github.com/python-eel/Eel) to provide browser-like GUI window. + +```bash +pip install touch-timestamp[eel] +``` + +![Browser interface](asset/eel-gui.avif "Eel interface") + +# Docs + +## Exact date + +When invoked with file paths, the program sets their modification times to the specified time. + +![Gui window](asset/mininterface-gui.avif "Graphical interface") + +## Fetch the time from the file name + +Should you end up with files that keep the date in the file name, use the `--from-name` parameter. + +```bash +$ touch-timestamp 20240828_160619.heic --from-name True +Changed 2001-01-01T12:00:00 → 2024-08-28T16:06:19: 20240828_160619.heic +``` + +## Full help + +Use the `--help` to see full options. + +```bash +$ touch-timestamp --help +usage: Touch [-h] [--eel | --no-eel] [--from-name {True,False}|STR] + [[PATH [PATH ...]]] + +╭─ positional arguments ─────────────────────────────────────────────────────╮ +│ [[PATH [PATH ...]]] │ +│ Files the modification date is to be changed. (default: ) │ +╰────────────────────────────────────────────────────────────────────────────╯ +╭─ options ──────────────────────────────────────────────────────────────────╮ +│ -h, --help │ +│ show this help message and exit │ +│ --eel, --no-eel │ +│ Prefer Eel GUI. (Set the date as in a chromium browser.) (default: │ +│ True) │ +│ --from-name {True,False}|STR │ +│ Fetch the modification time from the file names stem. Set the format │ +│ as for `datetime.strptime` like '%Y%m%d_%H%M%S'. │ +│ If set to True, the format will be auto-detected. │ +│ If a file name does not match the format or the format cannot be │ +│ auto-detected, the file remains unchanged. │ +│ │ +│ │ +│ Ex: `--from-name True 20240827_154252.heic` → modification time = │ +│ 27.8.2024 15:42 (default: False) │ +╰────────────────────────────────────────────────────────────────────────────╯ +``` + + +## Krusader user action + +To change the file timestamps easily from Krusader, import this [user action](extra/touch-timestamp-krusader-useraction.xml): `touch-timestamp %aList("Selected")%` \ No newline at end of file diff --git a/asset/eel-gui.avif b/asset/eel-gui.avif new file mode 100644 index 0000000..e8d9e03 Binary files /dev/null and b/asset/eel-gui.avif differ diff --git a/asset/mininterface-gui.avif b/asset/mininterface-gui.avif new file mode 100644 index 0000000..6f12515 Binary files /dev/null and b/asset/mininterface-gui.avif differ diff --git a/asset/textual.avif b/asset/textual.avif new file mode 100644 index 0000000..ea98c8b Binary files /dev/null and b/asset/textual.avif differ diff --git a/extra/touch-timestamp-krusader-useraction.xml b/extra/touch-timestamp-krusader-useraction.xml new file mode 100644 index 0000000..3392407 --- /dev/null +++ b/extra/touch-timestamp-krusader-useraction.xml @@ -0,0 +1,12 @@ + + + + + &touch-timestamp + touch-timestamp + Files + %aList("Selected")% + touch-timestamp %aList("Selected")% + Alt+Shift+T + + diff --git a/gui-touch.py b/gui-touch.py deleted file mode 100644 index d91f8ed..0000000 --- a/gui-touch.py +++ /dev/null @@ -1,29 +0,0 @@ -from os import utime -from pathlib import Path -from sys import argv - -import dateutil.parser -from eel import expose, init, start - -files = [Path(p) for p in argv[1:]] - -@expose -def get_len_files(): - return len(files) - -@expose -def get_first_file_date(): - return Path(files[0]).stat().st_mtime - -@expose -def set_timestamp(date, time): - print("Touching files", date, time) - print(", ".join(str(f) for f in files)) - if date and time: - time = dateutil.parser.parse(date + " " + time).timestamp() - [utime(f, (time, time)) for f in files] - return True - - -init(Path(__file__).absolute().parent.joinpath('static')) -start('index.html', size=(330, 30), port=0, block=True) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c00051a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "touch-timestamp" +version = "0.3.0" +description = "Change file timestamps with a dialog window." +authors = ["Edvard Rejthar "] +license = "GPL-3.0-or-later" +homepage = "https://github.com/CZ-NIC/touch-timestamp" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" +mininterface = "*" + +[tool.poetry.extras] +eel = ["eel"] + +[tool.poetry.scripts] +touch-timestamp = "touch_timestamp.touch_timestamp:main" \ No newline at end of file diff --git a/static/index.html b/touch_timestamp/static/index.html similarity index 100% rename from static/index.html rename to touch_timestamp/static/index.html diff --git a/touch_timestamp/touch_timestamp.py b/touch_timestamp/touch_timestamp.py new file mode 100755 index 0000000..84c8ed9 --- /dev/null +++ b/touch_timestamp/touch_timestamp.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +from dataclasses import dataclass, field +from datetime import datetime +from os import utime +from pathlib import Path + +import dateutil.parser +from mininterface import run +from tyro.conf import Positional + +try: + from eel import expose, init, start + eel = True +except ImportError: + eel = None + + +DateFormat = str # Use type as of Python3.12 + + +@dataclass +class Env: + files: Positional[list[Path]] = field(default_factory=list) + """ Files the modification date is to be changed. """ + + eel: bool = True + """ Prefer Eel GUI. (Set the date as in a chromium browser.) """ + + from_name: bool | DateFormat = False + """ + Fetch the modification time from the file names stem. Set the format as for `datetime.strptime` like '%Y%m%d_%H%M%S'. + If set to True, the format will be auto-detected. + If a file name does not match the format or the format cannot be auto-detected, the file remains unchanged. + + Ex: `--from-name True 20240827_154252.heic` → modification time = 27.8.2024 15:42 + """ + + +def set_files_timestamp(date, time, files: list[str]): + print("Touching files", date, time) + print(", ".join(str(f) for f in files)) + if date and time: + time = dateutil.parser.parse(date + " " + time).timestamp() + [utime(f, (time, time)) for f in files] + return True + + +def run_eel(files): + @expose + def get_len_files(): + return len(files) + + @expose + def get_first_file_date(): + return Path(files[0]).stat().st_mtime + + @expose + def set_timestamp(date, time): + return set_files_timestamp(date, time, files) + + init(Path(__file__).absolute().parent.joinpath('static')) + start('index.html', size=(330, 30), port=0, block=True) + + +def main(): + m = run(Env, prog="Touch") + + if m.env.from_name: + for p in m.env.files: + if m.env.from_name is True: # auto detection + try: + # 20240828_160619.heic -> "20240828 160619" -> "28.8." + dt = dateutil.parser.parse(p.stem.replace("_", "")) + except ValueError: + print(f"Cannot auto detect the date format: {p}") + continue + else: + try: + dt = datetime.strptime(p.stem, m.env.from_name) + except ValueError: + print(f"Does not match the format {m.env.from_name}: {p}") + continue + timestamp = int(dt.timestamp()) + original = datetime.fromtimestamp(p.stat().st_mtime) + utime(str(p), (timestamp, timestamp)) + print(f"Changed {original.isoformat()} → {dt.isoformat()}: {p}") + elif eel and m.env.eel: # set exact date with eel + run_eel(m.env.files) + else: # set exact date with Mininterface + if len(m.env.files) > 1: + title = f"Touch {len(m.env.files)} files" + else: + title = f"Touch {m.env.files[0].name}" + + with m: + m.title = title # NOTE: Changing title does not work + date = datetime.fromtimestamp(Path(m.env.files[0]).stat().st_mtime) + output = {title: {"date": str(date.date()), "time": str(date.time())}} + m.form(output) + set_files_timestamp(output["date"], output["time"], m.env.files) + + +if __name__ == "__main__": + main()