From 1ff8ded30c1bfae0d83d70205298f7d9b5c47d9d Mon Sep 17 00:00:00 2001 From: Edvard Rejthar Date: Tue, 3 Sep 2024 14:46:40 +0200 Subject: [PATCH] package --- .github/workflows/python-publish.yml | 23 ++++ Makefile | 5 + README.md | 82 +++++++++++++- asset/eel-gui.avif | Bin 0 -> 1902 bytes asset/mininterface-gui.avif | Bin 0 -> 2559 bytes asset/textual.avif | Bin 0 -> 2218 bytes extra/touch-timestamp-krusader-useraction.xml | 12 ++ gui-touch.py | 29 ----- pyproject.toml | 22 ++++ {static => touch_timestamp/static}/index.html | 0 touch_timestamp/touch_timestamp.py | 104 ++++++++++++++++++ 11 files changed, 245 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/python-publish.yml create mode 100644 Makefile create mode 100644 asset/eel-gui.avif create mode 100644 asset/mininterface-gui.avif create mode 100644 asset/textual.avif create mode 100644 extra/touch-timestamp-krusader-useraction.xml delete mode 100644 gui-touch.py create mode 100644 pyproject.toml rename {static => touch_timestamp/static}/index.html (100%) create mode 100755 touch_timestamp/touch_timestamp.py 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 0000000000000000000000000000000000000000..e8d9e035f89676284e733ee2446d2ea127cfc12d GIT binary patch literal 1902 zcmYjN2{_dG8~+Uk$)XuC$(dt~u&!JoOx6*z>L}kNVZ-A_4UMT1JEfQi(?#40_c;La-m# zy9fY005<|%9u5HTg+Tt12djVpq5(?~3_6?OEf3Ad^fe+Qv#sm|Bj|TW31JPW@ zfPzYM|iyZcUVrj0}5NeI#8X@E$V<9Xhj45;q)t^W)hBP1n#FOd9_^;#3f+XV69#AUOgb&7ZhY#T4gTdnk007=V zyoRSD03m*q!cuvWnocE;SSpO~^5|LG2pZ=i$Nbp6!_14W*~d`imP>0)(EqudvY~R= ziwIX<{G}q(n3h(UBogoaAkM%Yr#tP{^hdz%`DF*?R1jxg@|ZWK9-dGY^7wihV8!E& z*!bYV%GpbIl+@JP#8Mn9r*40HUwyObxTxk(_uwK1X)mkxcA>I3w9q3*>9~)uTfMJm zzMZh_IuBhIEzXCosBq=X4^$mQ%(OBa%v8@F=CI}UiA;@iktWFt2Rhr;k0>Y5x-mZ` zeQLX3re}qysl8hoqKHL}^FE{n*0*lqBK6+MX!}Tg^xr<8N81KFUy(OdQ>}CC)_3YM zK8R2k483LX8Sn7lm7TxUkmzKv(#kk9j~d6S%`7hOc|aDp&e*>p=+ z(3Jyc?3`@F%E5TmnLSeKVbn%yq{TO?)7|%8PTs4VIZRRO7V7`7L%&0KsAJ;gxI|A5 ziI}@{uZ_-7F8G5*baPS6Gg=0$FLCCI!w}8AP8{NCVDC3)k-&(uPOPnI!eQXF)$_T2 ziK(Vv*4HE@dNY(RZ@cYcClWV{LvO-och%Nflcc`SDUvtFNE>zc^lI&Y?%BD~C}Q)w z*IeDl@xg*py?I3A1qLFSWgi0ndE*COjWX8<&Uc=koLOyVX10~@Ok<0O`T9Gaw6GLf z5>4xZB}|<+L46opylJ#hpn7C{Xs@$J^*WaCS|rxMxmLw?e>S{49DsTX*>`h5ttZ4v4QKWt~ zXx|;-LRQPOi$kU7U?r$4=Sn+~D(l_7%FB|0Y*nJLY+8W~|2K~cIX=KTr zV`SNC%)LS1{E!eg%2;=oNXs2oM@4TfX&m6@HVc^UCo&83((t2igtAbfjSr0Azcy>B zw4Dvzjmg+u8&q4Yw~z#a=f?XVA}M(McVgJ^_PZT>*`Ew?`V}o;^ji8vbBllCX|rKhc-# z-hb{;Wq8W5z{9{9ZL6zl9wk1iTws`+u#0Mx4|54Cj6Lz=Yk2f&XH2agynWz_c)az& zkD{JRKc}RL5BX4g(+e8e6&>22vm!;y!uk?N`%C0ggjrG(FH>FEd-+{_)~#+)v5H$I zyxPyFOv1}$&^;qTLwW9fMJ4m1Z>I){7kXtre6}$?PC#a_cdLBwzS+NjBkcNY$VlZp ztYHZm*ke}{OKJ3b9b6nRXNBruyQR@uZ!N(m-enw?mG zIc5*Utzsk%?^{hNSmz}c7kU>skSm+^NSwJr#A~*jcJ4}4nn;?$n1GObwV9GH*xBrj zGyj~~&f=)un9VNp6s3*8$1V5@vA2d=Jm1tgzBgkiUoB%ft&UJOVpcI9mdoAmNoGTK)l4RRWZRP(3FqbgB literal 0 HcmV?d00001 diff --git a/asset/mininterface-gui.avif b/asset/mininterface-gui.avif new file mode 100644 index 0000000000000000000000000000000000000000..6f1251575e7cdc9de066c1ba2c1d11d3676b8c77 GIT binary patch literal 2559 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 SMg1spvtqT8vn$C+ + + + + &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()