Compare commits

...

49 Commits

Author SHA1 Message Date
779dab4f77 add bs4 in requirements 2025-07-13 07:17:49 -04:00
d872836624 fix: uri style page 2025-07-11 15:46:46 +02:00
df182fa7bb fix: readme 2025-07-11 15:44:24 +02:00
7d739ed777 add: doc photodown 2025-07-09 02:39:32 +02:00
6af8781aa2 add: bozodown doc 2025-07-09 02:20:58 +02:00
00bc97465f clean 2025-07-09 01:58:34 +02:00
74e99b96f0 clean: remove bulk_suffix
bulk_page -> page
bulk_category -> category
page -> collection
2025-07-09 01:57:52 +02:00
cbab99cc82 add: css to page 2025-07-09 00:03:03 +02:00
001a8c692e add: pictures menu in photo markdown 2025-07-08 22:08:24 +02:00
e1f08f0312 fix: indent and remove empty <p> 2025-07-08 22:05:41 +02:00
cf9cd77bcf add: use photodown to gen page 2025-07-08 18:35:58 +02:00
1563b3618a clean: create bozodown module 2025-07-08 16:31:53 +02:00
8c378492a3 bozodown: implement block 2025-07-08 15:10:17 +02:00
941e06e3d7 wip: implement page 2025-07-08 14:16:14 +02:00
6d6af200ab add: bozodown 2025-07-08 14:16:00 +02:00
4b4459cfbb add: update readme 2025-07-08 03:05:40 +02:00
ea75630fbb fix: rename album -> category 2025-07-08 02:58:19 +02:00
4ece9bf2ea add: multithread gen assets 2025-07-07 23:44:28 +02:00
67a83708cf clean: rename bulk_album category 2025-07-07 23:03:48 +02:00
a980dd8ead add: rename script 26 -> 00026 2025-06-15 00:31:54 +02:00
69023f1917 fix: protect touch 2025-05-22 17:53:05 +02:00
df05678ddc fix: sort file by name 2025-05-22 17:52:04 +02:00
bb29eb2b73 fix: do not recreate readme every time 2025-05-21 12:46:34 +02:00
c8f8698578 fix: raw_url 2025-05-06 23:16:37 +02:00
6236819e2b fix: rename general.html -> index.html 2025-05-06 23:14:58 +02:00
1a0391bafe fix: print 2025-05-06 23:13:02 +02:00
c7c2cf6409 add: loading bar on regen 2025-05-06 23:11:54 +02:00
26c60c5216 add: home page 2025-05-06 23:00:11 +02:00
6d1661131f clean: rename bulk 2025-05-06 22:54:18 +02:00
129dfe7df9 add: hashtag to album page to go directly on the picture you clicked 2025-05-06 22:42:22 +02:00
5fc12d5d33 fix: raw url 2025-05-06 22:12:53 +02:00
b895e5d85c add: albums link under pictures 2025-05-06 20:03:32 +02:00
b434d0837a add: regen parameter 2025-05-06 19:30:42 +02:00
b9f7090e96 add: center img without deform it 2025-05-06 19:24:56 +02:00
fe1f7d1485 add: thumb dimension is a global 2025-05-06 18:59:45 +02:00
2dd1fcfb0c add: bulk 2025-05-06 18:55:14 +02:00
8c2d5df0d2 fix: readme none 2025-05-04 19:17:43 +02:00
a551217592 fix: do not regen all small and thumb 2025-05-04 19:11:00 +02:00
5c773af571 copy use 0 padding 2025-05-04 18:35:56 +02:00
d0063c2cc4 core: gen thumb, small, readme for every image 2025-05-02 13:36:57 -04:00
08c0129af8 feat: use thumb on album page 2025-05-02 13:29:09 -04:00
de051f222d fix: copy only .NEF 2025-05-02 04:28:29 -04:00
ff56bac2cd clean: remove useless file 2025-05-01 14:53:44 -04:00
3f30154b36 add: album 2025-04-23 22:11:44 +02:00
b4a78a9e7d update readme 2025-04-23 22:11:27 +02:00
37808d6c48 fix: page linked list 2025-04-23 21:09:25 +02:00
70da0a2c28 add: path __str__ and __repr__ 2025-04-23 21:09:12 +02:00
bb0973917d recreate page 2025-04-23 20:49:58 +02:00
55bc1b36d1 add: docker nginx conf 2025-04-23 20:49:28 +02:00
44 changed files with 945 additions and 173 deletions

View File

@ -3,10 +3,10 @@ Just a simple python program to generate static site
## Screenshots ## Screenshots
### Picture Page ### Bulk Page
![](./screenshots/page.png) ![](./screenshots/page.png)
### Album Page ### Bulk Album Page
![](./screenshots/album.png) ![](./screenshots/album.png)
## Installation ## Installation
@ -17,32 +17,65 @@ python3 -m pip -r requirements.txt
## Usage ## Usage
Move and rename file Move and rename file with
```sh ```sh
python3 tools/copy.py %your raws% %site location% python3 tools/copy.py %your raws% %site location%/bulk/
``` ```
Here you can edit your pictures.
Generate html page Then
Generate html page with
``` sh ``` sh
python3 src/main.py %site location% python3 src/main.py %site location%
``` ```
## File ## File
### Mandatory ### File tree
File before `python src/main.py test_out`
```
test_out/
└── bulk
└── 00001
├── 00001.NEF
├── 00001.png
└── 00001.png.out.pp3
```
### Bulk
#### Mandatory
- a .png - a .png
### Optionnal #### Optionnal
- raw {*.NEF} - **raw** {*.NEF}
- readme.md - **readme.md**
- (png file).out.pp3 - (png file)**.out.pp3**: permit viewer to download it
### Futur update File after `python src/main.py test_out`
- Use summary for exif data and readme ```
- rename export file to rawtherapee profil test_out/
- add gimp profile file ├── bulk
- album │ └── 00001
- unrepertoried page │ ├── 00001_categories.txt
- fix: next and prev │ ├── 00001.NEF
- add audio file on page │ ├── 00001.png
- page mobile compatible │ ├── 00001.png.out.pp3
│ ├── 00001_small.jpg
│ ├── 00001_thumb.jpg
│ ├── page.html
│ └── readme.md
└── index.html
```
### Can be fill
#### _categories.txt
A file to organize your picture, 1 per line like
```
cats
portraits
```
#### readme.md**
A markdown file in the top of the bulk page

View File

@ -1,9 +0,0 @@
{
"file_type_extensions": {
"raw": [".NEF"],
"large": [".png"],
"small": [".jpeg"],
"edits": [".pp3"],
"desq": [".md"]
}
}

44
config/default.conf Normal file
View File

@ -0,0 +1,44 @@
server {
listen 80;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
autoindex on;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

20
doc/bozodown/block.md Normal file
View File

@ -0,0 +1,20 @@
# Block
## Usage
```
-----
style
---
content
-----
```
| name | usage |
| --- | --- |
| style | Your custom css, multiline supported |
| content | Your bozodown |
## Effect
Create a div with custom css

13
doc/bozodown/checkbox.md Normal file
View File

@ -0,0 +1,13 @@
# Checkbox
## Usage
```
- [ ] your text
or
- [x] your text
```
## Effect
![](./checkbox.png)

BIN
doc/bozodown/checkbox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

17
doc/bozodown/code.md Normal file
View File

@ -0,0 +1,17 @@
# Quote
## Usage
```
\```
your text
\```
```
## Effect
your text
to
```
your text
```

11
doc/bozodown/image.md Normal file
View File

@ -0,0 +1,11 @@
# Image
## Usage
```
![url]
```
## Effect
display the image

10
doc/bozodown/quote.md Normal file
View File

@ -0,0 +1,10 @@
# Quote
## Usage
```
`your text`
```
## Effect
your text -> `your text`

19
doc/bozodown/readme.md Normal file
View File

@ -0,0 +1,19 @@
# Bozodown
A markdown like, written in python, modulable
## Feature
### Text Formater
- [title](./title.md)
- [subtitle](./subtitle.md)
- [subsubtitle](./subsubtitle.md)
- [subsubsubtitle](./subsubsubtitle.md)
- [code](./code.md)
- [image](./image.md)
- [block](./block.md)
- [checkbox](./checkbox.md)
### Text Modifier
- [strong](./strong.md)
- [quote](./quote.md)

11
doc/bozodown/strong.md Normal file
View File

@ -0,0 +1,11 @@
# Strong
## Usage
```
**your text**
```
## Effect
your text -> **your text**

View File

@ -0,0 +1,13 @@
# Subsubsubtitle
## Usage
```
#### your text
```
## Effect
your text
to
#### your text

View File

@ -0,0 +1,13 @@
# Subsubtitle
## Usage
```
### your text
```
## Effect
your text
to
### your text

13
doc/bozodown/subtitle.md Normal file
View File

@ -0,0 +1,13 @@
# Subtitle
## Usage
```
## your text
```
## Effect
your text
to
## your text

13
doc/bozodown/title.md Normal file
View File

@ -0,0 +1,13 @@
# Title
## Usage
```
# your text
```
## Effect
your text
to
# your text

53
doc/photodown/gallery.md Normal file
View File

@ -0,0 +1,53 @@
# Block
## Usage
```
+++++
config
---
style
---
content
+++++
```
### Config
| type |
| --- |
| image-list |
| direction |
| --- |
| down |
| up |
| left |
| right |
### Style
Just css
### Content
```
![img1]
description 1
--
![img2]
description 2
--
...
```
| name | usage |
| --- | --- |
| style | Your custom css, multiline supported |
| content | Your bozodown |
## Effect
Create a group of image

16
doc/photodown/image.md Normal file
View File

@ -0,0 +1,16 @@
# Image
Permit you to use your own image without think about small, thumb, large, reference the bulk page. Always permit you to use external image with the url
## Usage
```
![./bulk/path_to_image/image_large_name_without_ext]
```
## Example
On my server I got this image `bulk/01613/01613.png`, so I will used put
```
![./bulk/01613/01613]
```
And the python will display an image clickable who link to the bulk page

11
doc/photodown/readme.md Normal file
View File

@ -0,0 +1,11 @@
# PhotoDown
Just a simple extension of [BozoDown](../bozodown/readme.md)
## Add
- [Pictures gallery]()
## Modify
- [image](./image.md)

6
doc/readme.md Normal file
View File

@ -0,0 +1,6 @@
# PhotoHUB
## Tools
### Generate page
To let user create his own page this project use [BozoDown](./bozodown/readme.md) with the [PhotoDown](./photodown/readme.md) extension

10
docker-compose.yml Normal file
View File

@ -0,0 +1,10 @@
version: "3.8"
services:
my-pictures:
image: nginx
volumes:
- ./test_out:/usr/share/nginx/html
- ./config:/etc/nginx/conf.d/
ports:
- 8089:80
networks: {}

View File

@ -1,3 +1,5 @@
beautifulsoup4==4.13.4
bs4==0.0.2
exif==1.6.1 exif==1.6.1
imageio==2.37.0 imageio==2.37.0
Jinja2==3.1.6 Jinja2==3.1.6
@ -8,3 +10,5 @@ pillow==11.1.0
plum-py==0.8.7 plum-py==0.8.7
progress==1.6 progress==1.6
rawpy==0.24.0 rawpy==0.24.0
soupsieve==2.7
typing_extensions==4.14.1

1
src/bozodown/__init__.py Normal file
View File

@ -0,0 +1 @@
__all__ = ["Bozodown"]

69
src/bozodown/bozodown.py Normal file
View File

@ -0,0 +1,69 @@
from collections.abc import Callable
from . import default_converters
from bs4 import BeautifulSoup
class Bozodown():
def __init__(self):
self._converters: dict[str, dict[str, str]] = default_converters.converters.copy()
self._text_converter: dict[str, str] = default_converters.text_converter.copy()
self._specific_case_namespaces: dict[str, Callable[[object, str, str], str]] = {
"bozodown": self._bozodown_render,
}
def render(self, to_parse: str):
content: str = ""
while len(to_parse) > 0:
text, converter = self._get_first_converter(to_parse)
content += self._render_element(text, converter)
to_parse = to_parse[len(text):]
return BeautifulSoup(content, 'html.parser').prettify()
def _render_element(self, text: str, converter: dict[str, str]) -> str:
code: str = converter.get("code")
if (code is not None):
namespace, id = code.split(":")
func = self._specific_case_namespaces[namespace]
return func(id, text)
start: int = len(converter["from_prefix"])
stop: int = len(text) - len(converter.get("from_suffix", ""))
if (text[start:stop] == "\n" and converter is self._text_converter):
return ""
return f"{converter['to_prefix']}{text[start:stop]}{converter['to_suffix']}"
def _bozodown_render(self, id: str, text: str) -> str:
if (id == "list"):
print("error: list not supported")
return ""
if (id == "block"):
style, to_parse = text[5:-5].split("---")
style = style.replace("\n", "")
content: str = self.render(to_parse)
return f"<div style='{style}'>{content}</div>"
def _get_first_converter(self, text: str) -> tuple[str, dict] | None:
first_converter_found: dict[str, str] | None = None
start: int | None = None
for converter in self._converters.values():
matching_patern_pos: str = text.find(converter['from_prefix'])
if (matching_patern_pos != -1):
if (first_converter_found is None or matching_patern_pos < start):
first_converter_found = converter
start = matching_patern_pos
if (first_converter_found is None):
return text, self._text_converter
if (start != 0):
return text[:start], self._text_converter
suffix: int = first_converter_found.get("from_suffix", "")
prefix: int = first_converter_found['from_prefix']
stop: int = text.find(suffix, start + len(prefix))
if (stop == -1):
print(f"error: '{prefix}' was never finished by a '{suffix}'")
return
stop += len(suffix)
return text[start:stop], first_converter_found

View File

@ -0,0 +1,78 @@
converters: list[str, dict[str, str]] = {
"title": {
"from_prefix": "# ",
"from_suffix": "\n",
"to_prefix": "<h1>",
"to_suffix": "</h1>",
},
"subtitle": {
"from_prefix": "## ",
"from_suffix": "\n",
"to_prefix": "<h2>",
"to_suffix": "</h2>",
},
"subsubtitle": {
"from_prefix": "### ",
"from_suffix": "\n",
"to_prefix": "<h3>",
"to_suffix": "</h3>",
},
"subsubsubtitle": {
"from_prefix": "#### ",
"from_suffix": "\n",
"to_prefix": "<h4>",
"to_suffix": "</h4>",
},
"quote": {
"from_prefix": "`",
"from_suffix": "`",
"to_prefix": "<p><code>",
"to_suffix": "</code></p>",
},
"bold": {
"from_prefix": "**",
"from_suffix": "**",
"to_prefix": "<strong>",
"to_suffix": "</strong>",
},
"code": {
"from_prefix": "```",
"from_suffix": "```",
"to_prefix": "<pre><code>",
"to_suffix": "</code></pre>",
},
"image": {
"from_prefix": "![",
"from_suffix": "]",
"to_prefix": "<img src='",
"to_suffix": "'>",
},
"block": {
"from_prefix": "-----",
"from_suffix": "-----",
"code": "bozodown:block",
},
"checkbox_clear": {
"from_prefix": "- [ ] ",
"from_suffix": "\n",
"to_prefix": "<li><input type='checkbox' disabled> ",
"to_suffix": "</li>",
},
"checkbox_full": {
"from_prefix": "- [x] ",
"from_suffix": "\n",
"to_prefix": "<li><input type='checkbox' checked disabled> ",
"to_suffix": "</li>",
},
"newline": {
"from_prefix": "<br>",
"from_suffix": "",
"to_prefix": "<br>",
},
}
text_converter: dict[str, str] = {
"from_prefix": "",
"to_prefix": "<p>",
"to_suffix": "</p>",
}

View File

@ -10,23 +10,23 @@ if TYPE_CHECKING:
from path import Path from path import Path
env = Environment(loader=FileSystemLoader('src/templates')) env = Environment(loader=FileSystemLoader('src/templates'))
album_template = env.get_template('album.jinja') category_template = env.get_template('category.jinja')
class Album(): class Category():
def __init__(self, name: str, albums_path: Path, pictures: list[Picture] = None, is_repertoried: bool = False): def __init__(self, name: str, categorys_path: Path, pictures: list[Picture] = None, is_repertoried: bool = False):
self._name: str = name self.name: str = name
self._pictures: list[Picture] = pictures or [] self._pictures: list[Picture] = pictures or []
self._is_repertoried: bool = is_repertoried self._is_repertoried: bool = is_repertoried
self._path: Path = Path(albums_path, f"{name}.html") self.path: Path = Path(categorys_path, f"{name}.html")
def add_picture(self, picture: Picture) -> None: def add_picture(self, picture: Picture) -> None:
self._pictures.append(picture) self._pictures.append(picture)
def _to_html(self) -> str|None: def _to_html(self) -> str|None:
html_rendered = album_template.render(album=self) html_rendered = category_template.render(category=self)
return html_rendered return html_rendered
def create(self) -> Path: def create(self) -> Path:
with open(self._path.get_absolute_path(), "w") as f: with open(self.path.get_absolute_path(), "w") as f:
f.write(self._to_html()) f.write(self._to_html())

35
src/collection.py Normal file
View File

@ -0,0 +1,35 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import config
from path import Path
if TYPE_CHECKING:
from photodown import Photodown
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('src/templates'))
page_template = env.get_template('collection.jinja')
class Collection:
def __init__(self, path: Path, markdown: Photodown):
self._path: Path = path
self._html: Path = Path(self._path.get_absolute_path()[:-len(config.COLLECTION_EXT)] + ".html")
self._markdown: Photodown = markdown
with open(path.get_absolute_path(), "r") as f:
self._raw_content: str = f.read()
def _get_content(self):
content = self._markdown.render(self._raw_content)
return content
def _to_html(self) -> str:
html: str = page_template.render(content=self._get_content())
return html
def create(self) -> Path:
with open(self._html.get_absolute_path(), "w") as f:
f.write(self._to_html())

View File

@ -1,3 +1,6 @@
CREATE_GENERAL_ALBUM: bool = True CREATE_GENERAL_CATEGORY: bool = True
THUMB_DIMENSION: tuple[int, int] = (200, 200)
MAX_THREADS: int = 50
COLLECTION_EXT: str = ".ph"

View File

@ -1,19 +1,26 @@
import sys
import shutil
import os
from progress.bar import Bar from progress.bar import Bar
from path import Path from path import Path
from picture import Picture from picture import Picture
from page import Page from page import Page
from album import Album from category import Category
from collection import Collection
from photodown import Photodown
import argparse
import config import config
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('src/templates'))
home_template = env.get_template('home.jinja')
def argument_parsing(): def argument_parsing():
if (len(sys.argv) < 2): parser = argparse.ArgumentParser()
print("error: missing argument", file=sys.stderr) parser.add_argument("site_path", help="the folder of your site MUST BE ALWAYS BEFORT PARAMETERS", type=str)
exit(1) parser.add_argument("-r", '--regen', help="Regenerate (t)thumb (s)small", type=str)
return sys.argv[1] args = parser.parse_args()
return args
def scan_pages(folders: list[Path]) -> list[Page]: def scan_pages(folders: list[Path]) -> list[Page]:
pages: list[Page] = [] pages: list[Page] = []
@ -21,13 +28,15 @@ def scan_pages(folders: list[Path]) -> list[Page]:
with Bar("Scanning Pages...", max=len(folders)) as bar: with Bar("Scanning Pages...", max=len(folders)) as bar:
for folder in folders: for folder in folders:
files: list[Path] = folder.get_files() files: list[Path] = folder.get_files()
page: Page = Page(folder, folder.get_name(), prev=prev) page: Page = Page(folder, folder.get_name(), prev=prev)
raw: Path = Path(folder, folder.get_name() + ".NEF") raw: Path = Path(folder, folder.get_name() + ".NEF")
id: int = 0
for file in files: for file in files:
if file.get_name().endswith(".png"): if file.get_name().endswith(".png"):
page.add_picture(Picture(file, page=page, raw=raw)) page.add_picture(Picture(file, id, page=page, raw=raw))
id += 1
if len(page.get_pictures()) == 0: if len(page.get_pictures()) == 0:
bar.next() bar.next()
@ -49,40 +58,91 @@ def create_pages(pages: list[Page]) -> None:
page.create() page.create()
bar.next() bar.next()
def scan_albums(pages: list[Page], albums_path: Path) -> list[Album]: def scan_categories(pages: list[Page], categories_path: Path) -> list[Category]:
albums: dict[str, Album] = {} categories: dict[str, Category] = {}
with Bar("Scanning pages...", max=len(pages)) as bar: with Bar("Scanning pages...", max=len(pages)) as bar:
for page in pages: for page in pages:
for picture in page.get_pictures(): for picture in page.get_pictures():
for album_name in picture.get_albums_name(): for category_name in picture.get_categories_name():
album: Album | None = albums.get(album_name) category: Category | None = categories.get(category_name)
if (album is None): if (category is None):
album = Album(album_name, albums_path) category = Category(category_name, categories_path)
albums.update({album_name: album}) categories.update({category_name: category})
album.add_picture(picture) picture.categories.append(category)
category.add_picture(picture)
bar.next() bar.next()
return (albums.values()) return (categories.values())
def create_albums(albums: list[Album]) -> None: def create_categories(categories: list[Category]) -> None:
with Bar("Generating albums...", max=len(albums)) as bar: with Bar("Generating categories...", max=len(categories)) as bar:
for album in albums: for category in categories:
album.create() category.create()
bar.next() bar.next()
def gen_bulk(bulk_path: Path, markdown: Photodown):
category_path: Path = Path(bulk_path, "categories")
pages: list[Page] = scan_pages(bulk_path.get_dirs())
categories: list[Category] = scan_categories(pages, category_path)
bulk: list[Picture] = []
for page in pages:
for picture in page.get_pictures():
bulk.append(picture)
markdown.add_bulk(bulk)
if config.CREATE_GENERAL_CATEGORY:
for category in categories:
if (category.name == "general"):
category.path = Path(bulk_path, "index.html")
Path("./src/templates/page.css").copy_to(Path(bulk_path, "page.css"))
create_pages(pages)
Path("./src/templates/category.css").copy_to(Path(bulk_path, "category.css"))
if (not category_path.exist()):
category_path.create()
create_categories(categories)
def scan_collections(site_path: Path, markdown: Photodown):
for path in [f for f in site_path.get_files() if f.get_name().endswith(config.COLLECTION_EXT)]:
collection: Collection = Collection(path, markdown)
collection.create()
def gen_collections(site_path: Path, markdown: Photodown):
scan_collections(site_path, markdown)
Path("./src/templates/collection.css").copy_to(Path(site_path, "collection.css"))
def regen(_path: Path, is_thumb: bool, is_small: bool):
pages: list[Page] = scan_pages(_path.get_dirs())
with Bar("Regenerating assets...", max=len(pages)) as bar:
for page in pages:
for picture in page.get_pictures():
if is_thumb:
picture.gen_thumb()
if is_small:
picture.gen_small()
bar.next()
def gen_home(site_path: Path) -> None:
home_path = Path(site_path, "index.html")
with open(home_path.get_absolute_path(), "w") as f:
f.write(home_template.render())
def main(): def main():
site_path = Path(argument_parsing()) args = argument_parsing()
pages: list[Page] = scan_pages(site_path.get_dirs()) site_path: Path = Path(args.site_path)
shutil.copy2("./src/templates/page.css", site_path.get_absolute_path()) bulk_path: Path = Path(site_path, "bulk/")
create_pages(pages) if args.regen is not None:
album_path: Path = Path(site_path, "albums") regen(bulk_path, 't' in args.regen, 's' in args.regen)
if (not album_path.exist()): markdown = Photodown()
album_path.create() gen_bulk(bulk_path, markdown)
albums: list[Album] = scan_albums(pages, album_path) gen_collections(site_path, markdown)
create_albums(albums) gen_home(site_path)
if config.CREATE_GENERAL_ALBUM:
shutil.move(os.path.join(album_path.get_absolute_path(), "general.html"), os.path.join(site_path.get_absolute_path(), "index.html"))
shutil.copy2("./src/templates/album.css", site_path.get_absolute_path())
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -9,7 +9,6 @@ from path import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from page import Page
from picture import Picture from picture import Picture
env = Environment(loader=FileSystemLoader('src/templates')) env = Environment(loader=FileSystemLoader('src/templates'))
@ -27,6 +26,8 @@ class Page():
self.html: Path = Path(self._path, "page.html") self.html: Path = Path(self._path, "page.html")
self.prev: Page = prev self.prev: Page = prev
self.next: Page = next self.next: Page = next
if (not self._readme.exist()):
self._readme.touch()
def add_picture(self, picture: Picture) -> None: def add_picture(self, picture: Picture) -> None:
self._pictures.append(picture) self._pictures.append(picture)
@ -47,6 +48,8 @@ class Page():
return None return None
with open(self._readme.get_absolute_path(), 'r') as f: with open(self._readme.get_absolute_path(), 'r') as f:
text = f.read() text = f.read()
if len(text) == 0:
return None
html = markdown.markdown(text) html = markdown.markdown(text)
return html return html
@ -62,6 +65,13 @@ class Page():
if (self._gen_exif()): if (self._gen_exif()):
return None return None
return self._exif return self._exif
def render_exif(self):
if not self.get_exif():
return None
with open(self._exif.get_absolute_path()) as f:
return f.read()
def get_raw(self): def get_raw(self):
return self._raw return self._raw

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import os import os
import sys import sys
import shutil
class Path(): class Path():
def __init__(self, *paths: str | Path): def __init__(self, *paths: str | Path):
@ -31,6 +31,11 @@ class Path():
return None return None
return self._name return self._name
def touch(self) -> None:
if (self.exist()):
return None
open(self.get_absolute_path(), "w+").close()
def create(self) -> None: def create(self) -> None:
os.makedirs(self.get_absolute_path()) os.makedirs(self.get_absolute_path())
@ -39,7 +44,10 @@ class Path():
def get_dirs(self) -> list[Path]: def get_dirs(self) -> list[Path]:
dirs: list[Path] = [] dirs: list[Path] = []
for element in os.listdir(self.get_absolute_path()): elements = os.listdir(self.get_absolute_path())
elements.sort()
elements.reverse()
for element in elements:
path: Path = Path(self._absolute_path, element) path: Path = Path(self._absolute_path, element)
if (os.path.isdir(path.get_absolute_path())): if (os.path.isdir(path.get_absolute_path())):
dirs.append(path) dirs.append(path)
@ -47,8 +55,24 @@ class Path():
def get_files(self) -> list[Path]: def get_files(self) -> list[Path]:
files: list[Path] = [] files: list[Path] = []
for element in os.listdir(self.get_absolute_path()): elements = os.listdir(self.get_absolute_path())
elements.sort()
elements.reverse()
for element in elements:
path: Path = Path(self._absolute_path, element) path: Path = Path(self._absolute_path, element)
if (os.path.isfile(path.get_absolute_path())): if (os.path.isfile(path.get_absolute_path())):
files.append(path) files.append(path)
return files return files
def copy_to(self, destination: Path) -> None:
shutil.copy2(self.get_absolute_path(), destination.get_absolute_path())
def __str__(self):
return self._absolute_path
def __repr__(self):
return f"Path({self._absolute_path})"
def __eq__(self, value):
if (isinstance(value, Path)):
self = Path(value.get_absolute_path())

51
src/photodown.py Normal file
View File

@ -0,0 +1,51 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from bozodown.bozodown import Bozodown
if TYPE_CHECKING:
from picture import Picture
class Photodown(Bozodown):
def __init__(self):
super().__init__()
self._specific_case_namespaces.update({"photohub": self._photohub_render})
image_converter = self._converters.get("image")
image_converter.update({"code": "photohub:image"})
self._converters.update({"pictures": {"from_prefix": "+++++","from_suffix": "+++++","code": "photohub:pictures",}})
self._bulk: list[Picture] = []
def add_bulk(self, bulk: list[Picture]):
self._bulk = bulk.copy()
def _render_image(self, url: str) -> str:
if (url.startswith("./")):
for picture in self._bulk:
if url == "." + picture.get_large().get_url()[:-4]:
return f"""<a href={picture.get_page().html.get_url()}><img src='{picture.get_small().get_url()}'></a>"""
return f"<img src='{url}'>"
def _render_pictures(self, config: dict[str, str | int], to_parse: str):
segments: list[str] = to_parse.split("--")
content: str = ""
for segment in segments:
content += f"<div class='pictures-item'>{self.render(segment)}</div>"
return f"<div class='pictures-menu'>{content}</div>"
def _photohub_render(self, id: str, text: str) -> str:
if (id == "image"):
picture_url = f"{text[2:-1]}"
return self._render_image(picture_url)
if (id == "pictures"):
config, style, to_parse = text[5:-5].split("---")
style = style.replace("\n", "")
config = {
k: v for line in config.strip().splitlines()
if ": " in line
for k, v in [line.split(": ", 1)]
}
content: str = self._render_pictures(config, to_parse)
return f"<div style='{style}'>{content}</div>"

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from PIL import Image from PIL import Image
import os import thread_manager
from path import Path from path import Path
@ -11,39 +11,66 @@ import config
if TYPE_CHECKING: if TYPE_CHECKING:
from page import Page from page import Page
from album import Album from category import Category
class Picture(): class Picture():
def __init__(self, picture_path: Path, page = None, raw: Path|None = None, albums_name: list[str] = None, is_repertoried: bool = True): def __init__(self, picture_path: Path, id: int, page: Page = None, raw: Path|None = None, is_repertoried: bool = True):
self._large: Path = picture_path self._large: Path = picture_path
self._small: Path = Path(picture_path.get_absolute_path()[:-4] + "_small.jpg") self._small: Path = Path(picture_path.get_absolute_path()[:-4] + "_small.jpg")
self._export_file: Path = Path(picture_path.get_absolute_path() + ".out.pp3") self._thumb: Path = Path(picture_path.get_absolute_path()[:-4] + "_thumb.jpg")
self._profile_file: Path = Path(picture_path.get_absolute_path() + ".out.pp3")
self._categories_file: Path = Path(picture_path.get_absolute_path()[:-4] + "_categories.txt")
self._raw: Path|None = raw self._raw: Path|None = raw
self.id: int = id
self._page: Page = page self._page: Page = page
self._albums_name: list[str] = albums_name or [] self._categories_name: list[str] = []
if (config.CREATE_GENERAL_ALBUM): self.categories: list[Category] = []
self._albums_name.append("general") if self._categories_file.exist():
with open(self._categories_file.get_absolute_path(), "r+") as f:
categories_name: list[str] = f.read()
if (len(categories_name)):
self._categories_name += categories_name.split("\n")
else:
self._categories_file.touch()
if (config.CREATE_GENERAL_CATEGORY):
self._categories_name.append("general")
self._is_reperoried: bool = is_repertoried self._is_reperoried: bool = is_repertoried
self.get_small()
self.get_thumb()
def get_page(self): def get_page(self):
return self._page return self._page
def get_albums_name(self): def get_categories_name(self):
return self._albums_name return self._categories_name
def get_small(self): def get_small(self):
if not self._small.exist(): if not self._small.exist():
self.gen_small() self.gen_small()
return self._small return self._small
def get_thumb(self):
if not self._thumb.exist():
self.gen_thumb()
return self._thumb
def get_large(self): def get_large(self):
return self._large return self._large
def get_export_file(self): def get_profile_file(self):
return self._export_file return self._profile_file
def gen_thumb(self):
def _(_large: Path, _thumb: Path):
im = Image.open(_large.get_absolute_path())
im.thumbnail(config.THUMB_DIMENSION)
im.save(_thumb.get_absolute_path(), "JPEG")
thread_manager.add_to_queu(_, (self._large, self._thumb))
def gen_small(self): def gen_small(self):
im = Image.open(self._large.get_absolute_path()).convert("RGB") def _(_large: Path, _small: Path):
im.save(self._small.get_absolute_path(), quality=95, optimize=True) im = Image.open(_large.get_absolute_path()).convert("RGB")
im.save(_small.get_absolute_path(), quality=95, optimize=True)
thread_manager.add_to_queu(_, (self._large, self._small))

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="album.css">
</head>
<body>
<h1>{{ album._name }}</h1>
<div class="picture-container">
{% for picture in album._pictures %}
<a href="{{ picture.get_page().html.get_url() }}">
<img src="{{ picture.get_small().get_url() }}">
</a>
{% endfor %}
</div>
</body>
</html>

View File

@ -28,14 +28,13 @@ body {
} }
img { img {
width: 100%; object-fit: contain
height: auto;
} }
a { a {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; justify-content: center;
background-color: var(--content1); background-color: var(--content1);
padding: 10px; padding: 10px;
margin: 10px; margin: 10px;

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/bulk/category.css">
</head>
<body>
<h1>{{ category.name }}</h1>
<div class="picture-container">
{% for picture in category._pictures %}
<a href="{{ picture.get_page().html.get_url() }}#{{ picture.id }}">
<img src="{{ picture.get_thumb().get_url() }}">
</a>
{% endfor %}
</div>
</body>
</html>

View File

@ -0,0 +1,6 @@
img {
width: 1000px;
height: 1000px;
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/page.css">
</head>
<body>
{{ content }}
</body>
</html>

0
src/templates/home.css Normal file
View File

13
src/templates/home.jinja Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<body>
<a href="/bulk/"><h1>See all pictures I take</h1></a>
<ul>
{% for collection in collections %}
<li>
<a href="{{collection.get_url() }}">{{ collection.get_name()}} </a>
</li>
{% endfor %}
</ul>
</body>
</html>

View File

@ -10,61 +10,78 @@
} }
body { body {
display: flex;
justify-content: center;
flex-direction: column;
width: 50%;
background-color: var(--bg1); background-color: var(--bg1);
margin: auto; display: flex;
margin-top: 10px; flex-direction: column;
margin-bottom: 50px; align-items: center;
color: lightgray; margin: 15% 15%;
} margin-top: 5%;
height: auto;
body * {
width: 100%;
}
.picture-container {
margin-top: 10px;
}
.picture-container-element {
margin-top: 0px;
} }
.readme { .readme {
border: 1px solid;
background-color: var(--bg2); background-color: var(--bg2);
} }
.readme-content { .picture-container {
margin: 30px;
}
.export,
.next {
margin-top: 0px;
display: block;
text-align: right;
}
.download {
margin-top: 10px;
background-color: var(--bg2); background-color: var(--bg2);
text-align: center; padding: 10px;
padding-bottom: 30px;
} }
.navigation, .picture {
.meta-picture { padding: 5px;
background-color: var(--content1);
}
.albums_container {
margin-right: 0;
}
img {
max-width: 100%;
height: auto;
}
.meta-picture, .navigation {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
width: 100%;
}
.profile {
text-align: right;
} }
a { a {
color: white; color: white;
text-decoration: none; text-decoration: none;
font-size: 1.5rem; font-size: 1.5rem;
}
@media screen and (max-width:767px) {
body {
margin: 0% 0%;
}
}
.exif, .readme {
width: 100%;
display: block;
}
.download {
width: 100%;
background-color: var(--bg2);
text-align: center;
}
.exif summary {
text-align: center;
}
.exif details {
display: flex;
justify-content: center;
background-color: var(--bg2);
} }

View File

@ -2,47 +2,67 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="/page.css"> <link rel="stylesheet" href="/bulk/page.css">
</head> </head>
<body> <body>
<div class="navigation"> <div class="navigation">
{% if page.prev %} {% if page.prev %}
<a href="{{ page.next.html.get_url() }}">Prev</a> <a href="{{ page.prev.html.get_url() }}">Prev</a>
{% endif %} {% endif %}
{% if page.next %} {% if page.next %}
<a class="next" href="{{ page.next.html.get_url() }}">Next</a> <a class="next" href="{{ page.next.html.get_url() }}">Next</a>
{% endif %} {% endif %}
</div> </div>
{% if page._readme.exist() %} {% if page.render_readme() %}
<div class="readme"> <div class="readme">
<div class="readme-content"> <details>
{{page.render_readme()}} <summary>
</div> README
</summary>
<div class="readme-content">
{{page.render_readme()}}
</div>
</details>
</div> </div>
{% endif %} {% endif %}
{% for picture in page._pictures %}
<div class="picture-container"> <div class="picture-container">
<div class="picture-container-element"> {% for picture in page._pictures %}
<img src='{{ picture.get_small().get_url() }}'> <div class="picture-container-item" id="{{ picture.id}}">
<div class="meta-picture"> <div class="picture">
<a href="{{ picture.get_large().get_url() }}">Large</a> <a href="{{ picture.get_small().get_url() }}">
{% if picture._export_file.exist() %} <img src='{{ picture.get_small().get_url() }}'>
<a class="export" href="{{ picture.get_export_file().get_url() }}">export file</a> </a>
{% endif %} <div class="meta-picture">
<a href="{{ picture.get_large().get_url() }}">Large</a>
{% if picture._profile_file.exist() %}
<a class="profile" href="{{ picture.get_profile_file().get_url() }}">Raw therapee profile</a>
{% endif %}
</div>
</div>
<div class="categories_container">
<h1>Categories:</h1>
{% for category in picture.categories %}
<a href="{{ category.path.get_url() }}" class="category_item">{{ al }}</a>
{% endfor %}
</div> </div>
</div> </div>
{% endfor %}
</div> </div>
{% endfor %} {% if page.get_raw().exist() %}
{% if page.get_raw().exist() or page.get_exif() %}
<div class="download"> <div class="download">
<h1>Download</h1> <h1>Download</h1>
{% if page.get_raw().exist() %} <a href="{{ page.get_raw().get_url() }}">RAW</a>
<a href="{{ page.get_raw() }}">RAW</a> </div>
{% endif %} {% endif %}
{% if page.get_exif() %} {% if page.get_exif() %}
<a href="{{ page.get_exif().get_url() }}">Exif</a> <div class="exif">
{% endif %} <details>
<summary>Exif</summary>
<pre>
{{ page.render_exif() }}
</pre>
</details>
</div> </div>
{% endif %} {% endif %}
</body> </body>

8
src/thread_manager.py Normal file
View File

@ -0,0 +1,8 @@
import concurrent.futures
import config
_pool = concurrent.futures.ThreadPoolExecutor(max_workers=config.MAX_THREADS)
def add_to_queu(func: callable, args: tuple):
global _pool
_pool.submit(func, *args)

View File

@ -33,7 +33,8 @@ def main():
copy_path = os.path.join(output_folder, "ph_copy.txt") copy_path = os.path.join(output_folder, "ph_copy.txt")
copy_info: dict[str, str] = get_copy_info(copy_path) copy_info: dict[str, str] = get_copy_info(copy_path)
files: list[str] = [os.path.join(input_folder, f) for f in os.listdir(input_folder)] files: list[str] = [os.path.join(input_folder, f) for f in os.listdir(input_folder)]
files = [f for f in files if os.path.isfile(f)] files = [f for f in files if os.path.isfile(f) and f.endswith(".NEF")]
files.sort()
with Bar("copying...", max=len(files)) as bar: with Bar("copying...", max=len(files)) as bar:
for file in files: for file in files:
with open(file, 'rb') as f: with open(file, 'rb') as f:
@ -42,7 +43,7 @@ def main():
hash: str = hashlib.sha256(image_data).hexdigest() hash: str = hashlib.sha256(image_data).hexdigest()
if copy_info.get(hash) is None: if copy_info.get(hash) is None:
new_name: str = str(len(copy_info)) new_name: str = f"{len(copy_info):05d}"
path: str = os.path.join(output_folder, new_name) path: str = os.path.join(output_folder, new_name)
os.makedirs(path) os.makedirs(path)

17
tools/rename.py Normal file
View File

@ -0,0 +1,17 @@
import shutil
import sys
import os
from progress.bar import Bar
def main():
folders: list[str] = os.listdir(sys.argv[1])
with Bar("copying...", max=len(folders)) as bar:
for folder in folders:
if (len(folder) != 5):
if (folder.isdigit()):
new_name: str = f"{int(folder):05d}"
shutil.move(os.path.join(sys.argv[1], folder), os.path.join(sys.argv[1], new_name))
bar.next()
if __name__ == "__main__":
exit(main())