Compare commits

..

13 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
39 changed files with 763 additions and 435 deletions

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

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

View File

@ -1,139 +0,0 @@
from collections.abc import Callable
from typing import Any
def _photohub_render(id: str, text: str) -> str:
if (id == "img"):
return f"<img src='{text[2:-1]}'>"
def _bozodown_render(id: str, text: str) -> str:
if (id == "list"):
print("error: list not supported")
return ""
_specific_case_namespaces: dict[str, Callable[[str, str], str]] = {
"bozodown": _bozodown_render,
"photohub": _photohub_render,
}
_converters: list[dict[str, str]] = [
{
"from_prefix": "# ",
"from_suffix": "\n",
"to_prefix": "<h1>",
"to_suffix": "</h1>",
},
{
"from_prefix": "## ",
"from_suffix": "\n",
"to_prefix": "<h2>",
"to_suffix": "</h2>",
},
{
"from_prefix": "### ",
"from_suffix": "\n",
"to_prefix": "<h3>",
"to_suffix": "</h3>",
},
{
"from_prefix": "#### ",
"from_suffix": "\n",
"to_prefix": "<h4>",
"to_suffix": "</h4>",
},
{
"from_prefix": "`",
"from_suffix": "`",
"to_prefix": "<p><code>",
"to_suffix": "</code></p>",
},
{
"from_prefix": "**",
"from_suffix": "**",
"to_prefix": "<strong>",
"to_suffix": "</strong>",
},
{
"from_prefix": "- ",
"from_suffix": "\n",
"code": "bozodown:list",
},
{
"from_prefix": "```",
"from_suffix": "```",
"to_prefix": "<pre><code>",
"to_suffix": "</code></pre>",
},
{
"from_prefix": "![",
"from_suffix": "]",
"code": "photohub:img",
},
{
"from_prefix": "- [ ] ",
"from_suffix": "\n",
"to_prefix": "<li><input type='checkbox' disabled> ",
"to_suffix": "</li>",
},
{
"from_prefix": "- [x] ",
"from_suffix": "\n",
"to_prefix": "<li><input type='checkbox' checked disabled> ",
"to_suffix": "</li>",
},
{
"from_prefix": "<br>",
"from_suffix": "",
"to_prefix": "<br>",
},
]
_default_converter: dict[str, str] = {
"from_prefix": "",
"to_prefix": "<p>",
"to_suffix": "</p>",
}
def _render_element(text: str, converter: dict[str, str]) -> str:
code: str = converter.get("code")
if (code is not None):
namespace, id = code.split(":")
print(namespace, id)
func = _specific_case_namespaces[namespace]
return func(id, text)
print(converter)
start: int = len(converter["from_prefix"])
stop: int = len(text) - len(converter.get("from_suffix", ""))
return f"{converter['to_prefix']}{text[start:stop]}{converter['to_suffix']}"
def _get_first_converter(text: str) -> tuple[str, dict] | None:
first_converter_found: dict[str, str | Callable[[str, list[str]], Any]] | None = None
start: int | None = None
for converter in _converters:
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, _default_converter
if (start != 0):
return text[:start], _default_converter
suffix: int = first_converter_found.get("from_suffix", "")
prefix: int = first_converter_found['from_prefix']
stop: int = text.find(suffix, start)
if (stop == -1):
print(f"error: '{prefix}' was never finished by a '{suffix}'")
return
stop += len(suffix)
return text[start:stop], first_converter_found
def render(raw_content: str) -> str:
content: str = ""
to_parse: str = raw_content
while len(to_parse) > 0:
text, converter = _get_first_converter(to_parse)
content += _render_element(text, converter)
to_parse = to_parse[len(text):]
return content

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

@ -1,78 +0,0 @@
from __future__ import annotations
from jinja2 import Environment, FileSystemLoader
import markdown
import os
from path import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bulk_page import Page
from picture import Picture
env = Environment(loader=FileSystemLoader('src/templates'))
page_template = env.get_template('bulk_page.jinja')
class BulkPage():
def __init__(self, path: Path, name: str, pictures: list[Picture] = None, prev: BulkPage|None = None, next: BulkPage|None = None):
self.name: str = name
self._path: Path = path
self._pictures: list[Picture] = pictures or []
self._exif: Path = Path(self._path, "exif.txt")
self._readme: Path = Path(self._path, "readme.md")
self._raw: Path = Path(self._path, name + ".NEF")
self.html: Path = Path(self._path, "page.html")
self.prev: Page = prev
self.next: Page = next
if (not self._readme.exist()):
self._readme.touch()
def add_picture(self, picture: Picture) -> None:
self._pictures.append(picture)
def get_pictures(self) -> list[Picture]:
return self._pictures
def _to_html(self) -> str|None:
html_rendered = page_template.render(page=self)
return html_rendered
def create(self) -> Path:
with open(self.html.get_absolute_path(), "w") as f:
f.write(self._to_html())
def render_readme(self) -> str|None:
if not self._readme.exist():
return None
with open(self._readme.get_absolute_path(), 'r') as f:
text = f.read()
if len(text) == 0:
return None
html = markdown.markdown(text)
return html
def _gen_exif(self):
if self._raw.exist():
if os.system(f"exiftool {self._raw.get_absolute_path()} > {self._exif.get_absolute_path()} 2>/dev/null") == 0:
return 0
os.remove(self._exif.get_absolute_path())
return 1
def get_exif(self) -> Path | None:
if not self._exif.exist():
if (self._gen_exif()):
return None
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):
return self._raw

View File

@ -10,9 +10,9 @@ if TYPE_CHECKING:
from path import Path from path import Path
env = Environment(loader=FileSystemLoader('src/templates')) env = Environment(loader=FileSystemLoader('src/templates'))
category_template = env.get_template('bulk_category.jinja') category_template = env.get_template('category.jinja')
class BulkCategory(): class Category():
def __init__(self, name: str, categorys_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

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

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

View File

@ -1,12 +1,11 @@
import sys
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 bulk_page import BulkPage
from bulk_category import BulkCategory
from page import Page from page import Page
from category import Category
from collection import Collection
from photodown import Photodown
import argparse import argparse
import config import config
@ -23,14 +22,14 @@ def argument_parsing():
args = parser.parse_args() args = parser.parse_args()
return args return args
def scan_bulk_pages(folders: list[Path]) -> list[BulkPage]: def scan_pages(folders: list[Path]) -> list[Page]:
pages: list[BulkPage] = [] pages: list[Page] = []
prev: BulkPage = None prev: Page = None
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: BulkPage = BulkPage(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 id: int = 0
@ -53,62 +52,69 @@ def scan_bulk_pages(folders: list[Path]) -> list[BulkPage]:
pages[0].prev = pages[-1] pages[0].prev = pages[-1]
return pages return pages
def create_pages(pages: list[BulkPage]) -> None: def create_pages(pages: list[Page]) -> None:
with Bar("Generating Pages...", max=len(pages)) as bar: with Bar("Generating Pages...", max=len(pages)) as bar:
for page in pages: for page in pages:
page.create() page.create()
bar.next() bar.next()
def scan_categories(pages: list[BulkPage], categories_path: Path) -> list[BulkCategory]: def scan_categories(pages: list[Page], categories_path: Path) -> list[Category]:
categories: dict[str, BulkCategory] = {} 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 category_name in picture.get_categories_name(): for category_name in picture.get_categories_name():
category: BulkCategory | None = categories.get(category_name) category: Category | None = categories.get(category_name)
if (category is None): if (category is None):
category = BulkCategory(category_name, categories_path) category = Category(category_name, categories_path)
categories.update({category_name: category}) categories.update({category_name: category})
picture.categories.append(category) picture.categories.append(category)
category.add_picture(picture) category.add_picture(picture)
bar.next() bar.next()
return (categories.values()) return (categories.values())
def create_categories(categories: list[BulkCategory]) -> None: def create_categories(categories: list[Category]) -> None:
with Bar("Generating categories...", max=len(categories)) as bar: with Bar("Generating categories...", max=len(categories)) as bar:
for category in categories: for category in categories:
category.create() category.create()
bar.next() bar.next()
def gen_bulk(bulk_path: Path): def gen_bulk(bulk_path: Path, markdown: Photodown):
category_path: Path = Path(bulk_path, "categories") category_path: Path = Path(bulk_path, "categories")
pages: list[BulkPage] = scan_bulk_pages(bulk_path.get_dirs()) pages: list[Page] = scan_pages(bulk_path.get_dirs())
categories: list[BulkCategory] = scan_categories(pages, category_path) 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: if config.CREATE_GENERAL_CATEGORY:
for category in categories: for category in categories:
if (category.name == "general"): if (category.name == "general"):
category.path = Path(bulk_path, "index.html") category.path = Path(bulk_path, "index.html")
Path("./src/templates/bulk_page.css").copy_to(Path(bulk_path, "bulk_page.css")) Path("./src/templates/page.css").copy_to(Path(bulk_path, "page.css"))
create_pages(pages) create_pages(pages)
Path("./src/templates/bulk_category.css").copy_to(Path(bulk_path, "bulk_category.css")) Path("./src/templates/category.css").copy_to(Path(bulk_path, "category.css"))
if (not category_path.exist()): if (not category_path.exist()):
category_path.create() category_path.create()
create_categories(categories) create_categories(categories)
def scan_pages(site_path: Path): 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.PAGE_EXT)]: for path in [f for f in site_path.get_files() if f.get_name().endswith(config.COLLECTION_EXT)]:
page: Page = Page(path) collection: Collection = Collection(path, markdown)
page.create() collection.create()
def gen_pages(site_path: Path): def gen_collections(site_path: Path, markdown: Photodown):
scan_pages(site_path) scan_collections(site_path, markdown)
Path("./src/templates/collection.css").copy_to(Path(site_path, "collection.css"))
def regen(bulk_path: Path, is_thumb: bool, is_small: bool): def regen(_path: Path, is_thumb: bool, is_small: bool):
pages: list[BulkPage] = scan_bulk_pages(bulk_path.get_dirs()) pages: list[Page] = scan_pages(_path.get_dirs())
with Bar("Regenerating assets...", max=len(pages)) as bar: with Bar("Regenerating assets...", 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():
@ -132,8 +138,9 @@ def main():
bulk_path: Path = Path(site_path, "bulk/") bulk_path: Path = Path(site_path, "bulk/")
if args.regen is not None: if args.regen is not None:
regen(bulk_path, 't' in args.regen, 's' in args.regen) regen(bulk_path, 't' in args.regen, 's' in args.regen)
gen_bulk(bulk_path) markdown = Photodown()
gen_pages(site_path) gen_bulk(bulk_path, markdown)
gen_collections(site_path, markdown)
gen_home(site_path) gen_home(site_path)

View File

@ -1,28 +1,77 @@
from __future__ import annotations from __future__ import annotations
from jinja2 import Environment, FileSystemLoader
import markdown
import os
from path import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import config if TYPE_CHECKING:
import bozodown from picture import Picture
from path import Path
env = Environment(loader=FileSystemLoader('src/templates'))
page_template = env.get_template('page.jinja')
class Page: class Page():
def __init__(self, path: Path): def __init__(self, path: Path, name: str, pictures: list[Picture] = None, prev: Page|None = None, next: Page|None = None):
self.name: str = name
self._path: Path = path self._path: Path = path
self._html: Path = Path(self._path.get_absolute_path()[:-len(config.PAGE_EXT)] + ".html") self._pictures: list[Picture] = pictures or []
with open(path.get_absolute_path(), "r") as f: self._exif: Path = Path(self._path, "exif.txt")
self._raw_content: str = f.read() self._readme: Path = Path(self._path, "readme.md")
self._raw: Path = Path(self._path, name + ".NEF")
def _get_content(self): self.html: Path = Path(self._path, "page.html")
content = bozodown.render(self._raw_content) self.prev: Page = prev
return content self.next: Page = next
if (not self._readme.exist()):
self._readme.touch()
def _to_html(self) -> str: def add_picture(self, picture: Picture) -> None:
html: str = self._get_content() self._pictures.append(picture)
def get_pictures(self) -> list[Picture]:
return self._pictures
def _to_html(self) -> str|None:
html_rendered = page_template.render(page=self)
return html_rendered
def create(self) -> Path:
with open(self.html.get_absolute_path(), "w") as f:
f.write(self._to_html())
def render_readme(self) -> str|None:
if not self._readme.exist():
return None
with open(self._readme.get_absolute_path(), 'r') as f:
text = f.read()
if len(text) == 0:
return None
html = markdown.markdown(text)
return html return html
def create(self) -> Path: def _gen_exif(self):
with open(self._html.get_absolute_path(), "w") as f: if self._raw.exist():
f.write(self._to_html()) if os.system(f"exiftool {self._raw.get_absolute_path()} > {self._exif.get_absolute_path()} 2>/dev/null") == 0:
return 0
os.remove(self._exif.get_absolute_path())
return 1
def get_exif(self) -> Path | None:
if not self._exif.exist():
if (self._gen_exif()):
return None
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):
return self._raw

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

@ -10,11 +10,11 @@ from typing import TYPE_CHECKING
import config import config
if TYPE_CHECKING: if TYPE_CHECKING:
from bulk_page import BulkPage from page import Page
from bulk_category import BulkCategory from category import Category
class Picture(): class Picture():
def __init__(self, picture_path: Path, id: int, page: BulkPage = None, raw: Path|None = 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._thumb: Path = Path(picture_path.get_absolute_path()[:-4] + "_thumb.jpg") self._thumb: Path = Path(picture_path.get_absolute_path()[:-4] + "_thumb.jpg")
@ -22,9 +22,9 @@ class Picture():
self._categories_file: Path = Path(picture_path.get_absolute_path()[:-4] + "_categories.txt") 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.id: int = id
self._page: BulkPage = page self._page: Page = page
self._categories_name: list[str] = [] self._categories_name: list[str] = []
self.categories: list[BulkCategory] = [] self.categories: list[Category] = []
if self._categories_file.exist(): if self._categories_file.exist():
with open(self._categories_file.get_absolute_path(), "r+") as f: with open(self._categories_file.get_absolute_path(), "r+") as f:
categories_name: list[str] = f.read() categories_name: list[str] = f.read()

View File

@ -1,87 +0,0 @@
:root {
--bg1: #002b36;
--bg2: #073642;
--content1: #586e75;
--content2: #657b83;
--content3: #839496;
--content4: #93a1a1;
--lbg1: #eee8d5;
--lbg2: #fdf6e3;
}
body {
background-color: var(--bg1);
display: flex;
flex-direction: column;
align-items: center;
margin: 15% 15%;
margin-top: 5%;
height: auto;
}
.readme {
background-color: var(--bg2);
}
.picture-container {
background-color: var(--bg2);
padding: 10px;
}
.picture {
padding: 5px;
background-color: var(--content1);
}
.albums_container {
margin-right: 0;
}
img {
max-width: 100%;
height: auto;
}
.meta-picture, .navigation {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.profile {
text-align: right;
}
a {
color: white;
text-decoration: none;
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

@ -1,70 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/bulk/bulk_page.css">
</head>
<body>
<div class="navigation">
{% if page.prev %}
<a href="{{ page.prev.html.get_url() }}">Prev</a>
{% endif %}
{% if page.next %}
<a class="next" href="{{ page.next.html.get_url() }}">Next</a>
{% endif %}
</div>
{% if page.render_readme() %}
<div class="readme">
<details>
<summary>
README
</summary>
<div class="readme-content">
{{page.render_readme()}}
</div>
</details>
</div>
{% endif %}
<div class="picture-container">
{% for picture in page._pictures %}
<div class="picture-container-item" id="{{ picture.id}}">
<div class="picture">
<a href="{{ picture.get_small().get_url() }}">
<img src='{{ picture.get_small().get_url() }}'>
</a>
<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>
{% endfor %}
</div>
{% if page.get_raw().exist() %}
<div class="download">
<h1>Download</h1>
<a href="{{ page.get_raw().get_url() }}">RAW</a>
</div>
{% endif %}
{% if page.get_exif() %}
<div class="exif">
<details>
<summary>Exif</summary>
<pre>
{{ page.render_exif() }}
</pre>
</details>
</div>
{% endif %}
</body>
</html>

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="/bulk/bulk_category.css"> <link rel="stylesheet" href="/bulk/category.css">
</head> </head>
<body> <body>

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

View File

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

View File

@ -0,0 +1,87 @@
:root {
--bg1: #002b36;
--bg2: #073642;
--content1: #586e75;
--content2: #657b83;
--content3: #839496;
--content4: #93a1a1;
--lbg1: #eee8d5;
--lbg2: #fdf6e3;
}
body {
background-color: var(--bg1);
display: flex;
flex-direction: column;
align-items: center;
margin: 15% 15%;
margin-top: 5%;
height: auto;
}
.readme {
background-color: var(--bg2);
}
.picture-container {
background-color: var(--bg2);
padding: 10px;
}
.picture {
padding: 5px;
background-color: var(--content1);
}
.albums_container {
margin-right: 0;
}
img {
max-width: 100%;
height: auto;
}
.meta-picture, .navigation {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.profile {
text-align: right;
}
a {
color: white;
text-decoration: none;
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,11 +2,69 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="/page.css"> <link rel="stylesheet" href="/bulk/page.css">
</head> </head>
<body> <body>
<h1>{{ category.name }}</h1> <div class="navigation">
{% if page.prev %}
<a href="{{ page.prev.html.get_url() }}">Prev</a>
{% endif %}
{% if page.next %}
<a class="next" href="{{ page.next.html.get_url() }}">Next</a>
{% endif %}
</div>
{% if page.render_readme() %}
<div class="readme">
<details>
<summary>
README
</summary>
<div class="readme-content">
{{page.render_readme()}}
</div>
</details>
</div>
{% endif %}
<div class="picture-container">
{% for picture in page._pictures %}
<div class="picture-container-item" id="{{ picture.id}}">
<div class="picture">
<a href="{{ picture.get_small().get_url() }}">
<img src='{{ picture.get_small().get_url() }}'>
</a>
<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>
{% endfor %}
</div>
{% if page.get_raw().exist() %}
<div class="download">
<h1>Download</h1>
<a href="{{ page.get_raw().get_url() }}">RAW</a>
</div>
{% endif %}
{% if page.get_exif() %}
<div class="exif">
<details>
<summary>Exif</summary>
<pre>
{{ page.render_exif() }}
</pre>
</details>
</div>
{% endif %}
</body> </body>
</html> </html>