WIP: Production release #6
@ -6,22 +6,31 @@ on:
|
||||
- master
|
||||
- dev
|
||||
|
||||
env:
|
||||
ZOLA_VERSION: "0.19.2"
|
||||
HOST: ${{ secrets.SERVER_IP }}
|
||||
SSH_USERNAME: ${{ secrets.USERNAME }}
|
||||
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_KEY }}
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Zola
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: zola@0.17.2
|
||||
run: |
|
||||
wget https://github.com/getzola/zola/releases/download/v${ZOLA_VERSION}/zola-v${ZOLA_VERSION}-x86_64-unknown-linux-gnu.tar.gz
|
||||
tar -xvzf *.tar.gz
|
||||
|
||||
- name: Build Zola Website
|
||||
run: |
|
||||
zola build
|
||||
./zola build
|
||||
|
||||
- name: Run postbuild script
|
||||
run: bash ./postbuild.sh
|
||||
|
||||
- name: Set Destination Folder if MASTER
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
@ -31,14 +40,12 @@ jobs:
|
||||
if: ${{ github.ref == 'refs/heads/dev' }}
|
||||
run: echo "DEST_FOLDER=/srv/www/cz/filiprojek/dev" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to server
|
||||
uses: AEnterprise/rsync-deploy@v1.0.2
|
||||
env:
|
||||
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
|
||||
ARGS: "-e -c -r --delete"
|
||||
SERVER_PORT: 22
|
||||
FOLDER: "./public/"
|
||||
SERVER_IP: ${{ secrets.SERVER_IP }}
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
SERVER_DESTINATION: ${{ env.DEST_FOLDER }}
|
||||
- name: Deploy
|
||||
run: |
|
||||
apt update -y && apt-get install -y --no-install-recommends rsync
|
||||
eval "$(ssh-agent -s)"
|
||||
ssh-add - <<< "${SSH_PRIVATE_KEY}"
|
||||
mkdir -p ~/.ssh/
|
||||
ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts
|
||||
rsync -r --delete-after public/* "${SSH_USERNAME}@${HOST}:${{ env.DEST_FOLDER }}"
|
||||
|
||||
|
10
Makefile
Normal file
10
Makefile
Normal file
@ -0,0 +1,10 @@
|
||||
all: clean format build
|
||||
|
||||
format:
|
||||
biome format --write .
|
||||
|
||||
build:
|
||||
zola build
|
||||
|
||||
clean:
|
||||
rm -rf public/
|
15
biome.json
Normal file
15
biome.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
|
||||
"files": {
|
||||
"ignore": [".vscode/", "node_modules/", "public/"]
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": false,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
# The URL the site will be built for
|
||||
base_url = "https://www.filiprojek.cz"
|
||||
base_url = "https://dev.filiprojek.cz"
|
||||
title = "Filip Rojek"
|
||||
|
||||
compile_sass = true
|
||||
@ -17,8 +17,8 @@ smart_punctuation = true
|
||||
git = "https://git.filiprojek.cz/fr/website"
|
||||
nav_items = [
|
||||
{name="Home", path="/"},
|
||||
#{name="About", path="/about"},
|
||||
#{name="Projects", path="/projects/"},
|
||||
#{name="Posts", path="/posts"}
|
||||
{name="About", path="/about"},
|
||||
{name="Projects", path="/projects/"},
|
||||
{name="Posts", path="/posts"}
|
||||
]
|
||||
|
||||
|
27
content/about.md
Normal file
27
content/about.md
Normal file
@ -0,0 +1,27 @@
|
||||
+++
|
||||
title = "About"
|
||||
template = "about.html"
|
||||
+++
|
||||
|
||||
## About
|
||||
I am student of IT at Charles University in Prague (Faculty of Education).
|
||||
|
||||
My programming journey began in high school, where I developed a passion for `Linux` and co-founded [Fofrweb](https://fofrweb.com). Together with a classmate, I created web applications using `Node.js` and `Vue.js`, all hosted on my own Linux server.
|
||||
|
||||
As a member of [Microlab](https://microlab.space), the university's hacker space, I engage with fellow tech enthusiasts. Additionally, I contribute to the open-source community as a package maintainer for [Void Linux](https://voidlinux.org).
|
||||
|
||||
## Work
|
||||
I currently work as a Linux engineer. Most of my work involves Debian based systems - desktops (Raspberry Pi, Intel NUC) and servers.
|
||||
|
||||
I also create custom websites from time to time.
|
||||
|
||||
## Projects
|
||||
Most of my projects are hosted on Gitea and GitHub:
|
||||
- Gitea: [git.filiprojek.cz/fr](https://git.filiprojek.cz/fr)
|
||||
- GitHub: [github.com/filiprojek](https://github.com/filiprojek)
|
||||
|
||||
## Contact
|
||||
- <a href="mailto:filip@filiprojek.cz">filip@filiprojek.cz</a>
|
||||
- [@filiprojek](https://t.me/filiprojek) on Telegram
|
||||
- PGP: [0x7E65EA58C6075F09](https://keys.openpgp.org/vks/v1/by-fingerprint/CA3D9BE28315B49164130CD97E65EA58C6075F09)
|
||||
|
7
content/posts/_index.md
Normal file
7
content/posts/_index.md
Normal file
@ -0,0 +1,7 @@
|
||||
+++
|
||||
title = "Posts"
|
||||
template = "post_list.html"
|
||||
page_template = "post.html"
|
||||
sort_by = "date"
|
||||
+++
|
||||
|
66
content/posts/cs2_4-3_resolution.md
Normal file
66
content/posts/cs2_4-3_resolution.md
Normal file
@ -0,0 +1,66 @@
|
||||
+++
|
||||
title = "Fixing 4:3 Resolution in CS2 on Linux with NVIDIA GPU"
|
||||
date = 2024-10-17
|
||||
description = "How I fixed the 4:3 resolution in CS2 with an NVIDIA graphics card and Linux"
|
||||
+++
|
||||
|
||||
I don't consider myself a gamer, but I've been playing the Counter-Strike series since CS 1.6. Every now and then, I enjoy staying up all night playing this broken game.
|
||||
|
||||
Ever since I started playing Counter-Strike, I’ve preferred using a 4:3 stretched resolution on my 16:9 monitor. When I switched to Linux as my daily driver, the only game I really cared about was CS:GO.
|
||||
|
||||
CS:GO ran perfectly without any tweaks on my [Void Linux](https://voidlinux.org) system with a 1050ti laptop graphics card. I could play with the stretched resolution, and I even got more FPS than I did on Windows.
|
||||
|
||||
My problems started with the release of CS2. The 4:3 resolution didn’t work at all, some resolutions were missing, and there didn’t seem to be a solution (except for some Wayland fixes). There’s a [GitHub issue](https://github.com/ValveSoftware/csgo-osx-linux/issues/3264) about it.
|
||||
|
||||
My first idea was to set a custom resolution through `xrandr`. I followed [guides like this one](https://unix.stackexchange.com/questions/227876/how-to-set-custom-resolution-using-xrandr-when-the-resolution-is-not-available-i), but that didn’t work for me.
|
||||
|
||||
For a while, I just stuck with the standard 16:9 `1920x1080` resolution. But today, I opened the `nvidia-settings` GUI. After tinkering with some advanced settings in the resolution section, I think I finally fixed my issue.
|
||||
|
||||
In the `X Server Display Configuration` section under advanced options, I adjusted the ViewPortIn and Panning settings. I’m currently using a `2560x1440` resolution, so for 4:3 stretched, I set the resolution to `1440x1080`.
|
||||
|
||||
Here are the settings that worked for me:
|
||||
- **ViewPortIn**: `1440x1080`
|
||||
- **ViewPortOut**: `2560x1440+0+0`
|
||||
- **Panning**: `1440x1080`
|
||||
|
||||
That fixed the issue, but I didn’t want to manually open `nvidia-settings` every time I wanted to play CS2. After reading the [Arch Wiki article](https://wiki.archlinux.org/title/NVIDIA#Using_nvidia-settings) on `nvidia-settings`, I found that I could use this command to get the current resolution information:
|
||||
|
||||
```sh
|
||||
$ nvidia-settings -q CurrentMetaMode
|
||||
```
|
||||
|
||||
So ~I wrote~ ChatGPT wrote a small bash script to switch between my regular resolution and the CS2 resolution.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Define the commands for the two modes
|
||||
MODE1="nvidia-settings --assign 'CurrentMetaMode=DPY-4: 2560x1440_144 @2560x1440 +1920+0 {ViewPortIn=2560x1440, ViewPortOut=2560x1440+0+0}, DPY-3: nvidia-auto-select @1920x1080 +0+180 {ViewPortIn=1920x1080, ViewPortOut=1920x1080+0+0}'"
|
||||
MODE2="nvidia-settings --assign 'CurrentMetaMode=DPY-4: 2560x1440_144 @1440x1080 +0+0 {ViewPortIn=1440x1080, ViewPortOut=2560x1440+0+0}'"
|
||||
|
||||
# File to store the current mode
|
||||
STATE_FILE="/tmp/current_resolution_mode"
|
||||
|
||||
# Check if the state file exists
|
||||
if [[ ! -f "$STATE_FILE" ]]; then
|
||||
# If the state file doesn't exist, create it and set it to mode 1
|
||||
echo "1" > "$STATE_FILE"
|
||||
CURRENT_MODE=1
|
||||
else
|
||||
# Read the current mode from the state file
|
||||
CURRENT_MODE=$(cat "$STATE_FILE")
|
||||
fi
|
||||
|
||||
# Switch between the two modes
|
||||
if [[ "$CURRENT_MODE" -eq 1 ]]; then
|
||||
# Switch to mode 2
|
||||
eval "$MODE2"
|
||||
echo "2" > "$STATE_FILE"
|
||||
echo "Switched to resolution mode 2"
|
||||
else
|
||||
# Switch to mode 1
|
||||
eval "$MODE1"
|
||||
echo "1" > "$STATE_FILE"
|
||||
echo "Switched to resolution mode 1"
|
||||
fi
|
||||
```
|
196
content/posts/jellyfin-language-specific-library.md
Normal file
196
content/posts/jellyfin-language-specific-library.md
Normal file
@ -0,0 +1,196 @@
|
||||
+++
|
||||
title = "Creating a Language-Specific Jellyfin Library"
|
||||
date = 2024-12-14
|
||||
description = "How to set up a Jellyfin library for language-specific content"
|
||||
+++
|
||||
|
||||
Managing a multilingual media library can be a challenge, especially if you want to share specific language content with others. In my case, I have a large collection of movies and TV shows in both Czech and English. To help my parents enjoy only Czech-language content, I decided to create a dedicated Jellyfin library for it. Since Jellyfin doesn't natively support filtering libraries by language, I built a custom solution using Bash scripting and Docker.
|
||||
|
||||
## The Problem
|
||||
My media library has the following folder structure for movies:
|
||||
|
||||
```
|
||||
movies/
|
||||
├── Movie name (Year of release)
|
||||
│ ├── Movie name resolution.mkv
|
||||
│ └── Movie name resolution.nfo
|
||||
```
|
||||
|
||||
And for series:
|
||||
|
||||
```
|
||||
series/
|
||||
├── Series name
|
||||
│ └── Season 1
|
||||
│ ├── Series name - S01E01 - Pilot WEBRip-1080p.mkv
|
||||
│ ├── Series name - S01E01 - Pilot WEBRip-1080p.cs.srt
|
||||
│ └── Series name - S01E01 - Pilot WEBRip-1080p.en.srt
|
||||
```
|
||||
|
||||
For movies, each film is stored in its own folder. For series, episodes are nested within season folders, under the parent series folder. Jellyfin requires each movie or series to reside in its own directory, which means symlinks must point to the parent folder rather than the media files themselves.
|
||||
|
||||
Additionally, I needed to differentiate between movies and series when creating symlinks. For movies, the parent directory is the movie folder, but for series, the symlink should point to the series' root folder (not the season folder). Here's how I solved it.
|
||||
|
||||
## Custom Script for Language Filtering
|
||||
The following script processes the media directories to identify content with Czech audio tracks and creates symlinks to organize them into separate `cz_movies` and `cz_series` directories.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Define directories and language codes
|
||||
SOURCE_DIR_MOVIES="/media/movies"
|
||||
TARGET_DIR_MOVIES="/media/cz_movies"
|
||||
SOURCE_DIR_SERIES="/media/series"
|
||||
TARGET_DIR_SERIES="/media/cz_series"
|
||||
LANGUAGES_TO_CHECK=("cze" "cz" "Czech" "czech" "česky" "cs" "ces")
|
||||
FFPROBE="/usr/lib/jellyfin-ffmpeg/ffprobe"
|
||||
|
||||
# List of file extensions to skip
|
||||
SKIP_EXTENSIONS=("nfo" "srt" "sh")
|
||||
|
||||
# Function to process a source directory
|
||||
process_directory() {
|
||||
local SOURCE_DIR="$1"
|
||||
local TARGET_DIR="$2"
|
||||
local IS_SERIES="$3"
|
||||
|
||||
# Loop through all files in SOURCE_DIR and subdirectories
|
||||
find "$SOURCE_DIR" -type f | while read FILE; do
|
||||
# Get the file extension
|
||||
EXTENSION="${FILE##*.}"
|
||||
|
||||
# Check if the file extension is in the skip list
|
||||
if [[ " ${SKIP_EXTENSIONS[@]} " =~ " $EXTENSION " ]]; then
|
||||
# Skip the file if it's in the skip list
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Processing $FILE"
|
||||
|
||||
# Get the languages from the current file
|
||||
LANGUAGES=($($FFPROBE -v error -show_entries stream=codec_type:stream_tags=language:stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$FILE" | tr '\n' ' ' | grep -o 'audio [^ ]*' | awk '{print $2}'))
|
||||
|
||||
# Check if any of the languages in LANGUAGES_TO_CHECK exist in LANGUAGES
|
||||
FOUND=false
|
||||
for LANG_TO_CHECK in "${LANGUAGES_TO_CHECK[@]}"; do
|
||||
if [[ " ${LANGUAGES[@]} " =~ " $LANG_TO_CHECK " ]]; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if $FOUND; then
|
||||
echo "Found a matching language (${LANGUAGES_TO_CHECK[@]}) in the list"
|
||||
# Determine the symlink target based on whether this is a series or a movie
|
||||
if [ "$IS_SERIES" = true ]; then
|
||||
# For series, link the <Series name> directory
|
||||
PARENT_DIR=$(dirname "$(dirname "$FILE")")
|
||||
else
|
||||
# For movies, link the <Movie name> directory
|
||||
PARENT_DIR=$(dirname "$FILE")
|
||||
fi
|
||||
|
||||
# Create a symlink to the parent directory in TARGET_DIR
|
||||
ln -snf "$PARENT_DIR" "$TARGET_DIR/$(basename "$PARENT_DIR")"
|
||||
echo "Symlink \"$PARENT_DIR\" \"$TARGET_DIR/$(basename "$PARENT_DIR")\""
|
||||
echo "Symlink created for $PARENT_DIR"
|
||||
else
|
||||
echo "Skipping $FILE"
|
||||
fi
|
||||
|
||||
done
|
||||
}
|
||||
|
||||
# Process movies and series directories
|
||||
process_directory "$SOURCE_DIR_MOVIES" "$TARGET_DIR_MOVIES" false
|
||||
process_directory "$SOURCE_DIR_SERIES" "$TARGET_DIR_SERIES" true
|
||||
```
|
||||
|
||||
## Automating the Script with Docker and Cron
|
||||
To automate the script, I run it hourly using a minimal Docker container with cron installed. Instead of modifying the Jellyfin Docker image, I created a separate Alpine-based container to handle the job.
|
||||
|
||||
### Dockerfile for the Cron Container
|
||||
```Dockerfile
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache docker && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
CMD ["sh"]
|
||||
```
|
||||
|
||||
### Docker-Compose Setup
|
||||
Here's the updated `docker-compose.yml` that includes the Jellyfin service and the cron container:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
jellyfin:
|
||||
image: jellyfin/jellyfin:latest
|
||||
container_name: jellyfin
|
||||
|
||||
# ... your jellyfin docker compose ...
|
||||
|
||||
volumes:
|
||||
- ./czech-lib-cron.sh:/czech-lib-cron.sh:ro # the custom library script
|
||||
- /mnt/media/torrent:/media:ro # your media folder
|
||||
- /mnt/media/torrent/cz_movies:/media/cz_movies # the newely created language specific library folder
|
||||
- /mnt/media/torrent/cz_series:/media/cz_series # the newely created language specific library folder
|
||||
|
||||
jellyfin-cron:
|
||||
build:
|
||||
context: ../docker-cron
|
||||
dockerfile: Dockerfile
|
||||
container_name: jellyfin-cron
|
||||
# Run the custom library script every hour at 45 minutes past
|
||||
command: >
|
||||
/bin/sh -c "
|
||||
echo '45 * * * * docker exec -u root jellyfin bash -c \"/czech-lib-cron.sh\"' > /etc/crontabs/root &&
|
||||
crond -f -l 2"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock # Needed for `docker exec`
|
||||
networks:
|
||||
- default
|
||||
```
|
||||
|
||||
## Bonus: Fix Missing Audio Language Metadata
|
||||
Some files in my library lacked proper audio language metadata, causing them to be missed by the script. To fix this, I wrote another script to add the correct metadata:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# Root directory (current working directory)
|
||||
ROOT_DIR="$(pwd)"
|
||||
|
||||
# Function to process each video file
|
||||
process_file() {
|
||||
local input_file="$1"
|
||||
local temp_file="${input_file%.avi}_temp.avi"
|
||||
|
||||
echo "Processing: $input_file"
|
||||
|
||||
# Add metadata to the audio track and overwrite the original file
|
||||
ffmpeg -i "$input_file" -map 0 -c copy -metadata:s:a:0 language=cze "$temp_file"
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
mv "$temp_file" "$input_file"
|
||||
echo "Successfully updated: $input_file"
|
||||
else
|
||||
echo "Error updating: $input_file"
|
||||
rm -f "$temp_file" # Remove temporary file on failure
|
||||
fi
|
||||
}
|
||||
|
||||
# Export function for use in find's -exec
|
||||
export -f process_file
|
||||
|
||||
# Find all .avi files and process them
|
||||
find "$ROOT_DIR" -type f -name "*.avi" -exec bash -c 'process_file "$0"' {} \;
|
||||
```
|
||||
|
||||
This script re-encodes files to include the missing language metadata. It defaults to `.avi` files, but you can adjust it as needed.
|
||||
|
||||
## Conclusion
|
||||
|
||||
With this setup, you can create language-specific libraries in Jellyfin. Make sure to add the /media/cz_movies and /media/cz_series folders as paths for new libraries in the Jellyfin dashboard settings. After adding these libraries, simply scan them to see your filtered content. Happy watching!
|
||||
|
192
content/posts/jellyfin.md
Normal file
192
content/posts/jellyfin.md
Normal file
@ -0,0 +1,192 @@
|
||||
+++
|
||||
title = "Host Jellyfin with Docker and Docker Compose"
|
||||
date = 2024-10-03
|
||||
description = "Set up a Jellyfin media server with Docker, including hardware transcoding, media management, and companion services like Jellyseerr and Jellystat."
|
||||
+++
|
||||
|
||||
Running your own media server is a great way to have complete control over your media library, and Jellyfin is one of the best open-source media server solutions available today. With Docker and Docker Compose, you can efficiently manage and scale your Jellyfin instance, along with useful companion services like Jellyseerr and Jellystat for enhanced functionality.
|
||||
|
||||
In this article, I’ll walk through my setup of a Jellyfin media server, hosted in Docker using Docker Compose. This assumes you already have Docker and Docker Compose installed and are comfortable with Linux environments. I’ll cover the configuration of Jellyfin, hardware transcoding, and the integration of supporting services like Jellyseerr and Jellystat to manage requests and track server statistics.
|
||||
|
||||
## Docker Compose Configuration Breakdown
|
||||
Here’s the Docker Compose file I use to deploy Jellyfin and its associated services.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
jellyfin:
|
||||
image: jellyfin/jellyfin:latest
|
||||
container_name: jellyfin
|
||||
user: 1000:1000
|
||||
restart: 'unless-stopped'
|
||||
devices:
|
||||
- /dev/dri:/dev/dri # for hardware transcoding
|
||||
- /dev/dri/renderD128:/dev/dri/renderD128
|
||||
#- /dev/kfd:/dev/kfd # Remove this device if you don't use the OpenCL tone-mapping
|
||||
group_add:
|
||||
- "103" # render gid
|
||||
- "27" # video gid
|
||||
networks:
|
||||
- default
|
||||
ports:
|
||||
- 5100:8096
|
||||
volumes:
|
||||
- ./config:/config
|
||||
- ./cache:/cache
|
||||
- /mnt/media/torrent:/media:ro
|
||||
- /dev/shm:/data/transcode # Offload transcoding to RAM if you have enough RAM
|
||||
environment:
|
||||
- JELLYFIN_PublishedServerUrl=https://your.host.com
|
||||
|
||||
jellyseerr:
|
||||
image: fallenbagel/jellyseerr:latest
|
||||
container_name: jellyseerr
|
||||
environment:
|
||||
- LOG_LEVEL=debug
|
||||
- TZ=Europe/Prague
|
||||
ports:
|
||||
- 5280:5055
|
||||
volumes:
|
||||
- ./jellyseerr-config:/app/config
|
||||
restart: unless-stopped
|
||||
|
||||
jellystat-db:
|
||||
image: postgres:15.2
|
||||
container_name: jellystat-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: <password>
|
||||
volumes:
|
||||
- ./jellystat-db:/var/lib/postgresql/data
|
||||
|
||||
jellystat:
|
||||
image: cyfershepard/jellystat:latest
|
||||
container_name: jellystat
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: <password>
|
||||
POSTGRES_IP: jellystat-db
|
||||
POSTGRES_PORT: 5432
|
||||
JWT_SECRET: <jwt_secret>
|
||||
TZ: Europe/Prague
|
||||
volumes:
|
||||
- ./jellystat-data:/app/backend/backup-data
|
||||
ports:
|
||||
- "5110:3000"
|
||||
depends_on:
|
||||
- jellystat-db
|
||||
```
|
||||
|
||||
## Jellyfin Service
|
||||
The core of this setup is the jellyfin service. This container runs the Jellyfin server itself, and the configuration is designed to optimize performance and security.
|
||||
|
||||
### Hardware Transcoding
|
||||
For efficient media transcoding, I’ve configured Jellyfin to leverage the hardware capabilities of the host machine. Specifically, I’ve mounted the /dev/dri device to the container for Intel or AMD GPU transcoding:
|
||||
|
||||
```yaml
|
||||
devices:
|
||||
- /dev/dri:/dev/dri
|
||||
- /dev/dri/renderD128:/dev/dri/renderD128
|
||||
```
|
||||
|
||||
If you don’t need OpenCL tone-mapping, you can skip the /dev/kfd device. By adding the necessary GIDs (103 for render and 27 for video), the container has access to GPU resources.
|
||||
|
||||
Additionally, I’ve offloaded transcoding operations to the system’s RAM by mounting /dev/shm as the transcode directory:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- /dev/shm:/data/transcode
|
||||
```
|
||||
|
||||
This is particularly useful if you have sufficient RAM, as it improves transcoding performance by avoiding disk I/O overhead.
|
||||
|
||||
### Storage
|
||||
In the `volumes` section, I’ve mounted the following directories:
|
||||
|
||||
- `./config:/config`: Stores Jellyfin’s configuration data.
|
||||
- `./cache:/cache`: Holds cache data to improve performance.
|
||||
- `/mnt/media/torrent:/media:ro`: This is the read-only mount where Jellyfin accesses my media library. It’s pointed to my torrent directory, ensuring that Jellyfin can index and serve files from there but not modify them.
|
||||
|
||||
### Network and Ports
|
||||
|
||||
The Jellyfin server is exposed on port `5100` (mapped to Jellyfin’s default 8096 internal port). I also set the JELLYFIN_PublishedServerUrl environment variable to make sure the correct public URL is used for generating media links and external access.
|
||||
|
||||
## Jellyseerr for Media Requests
|
||||
|
||||
Jellyseerr is a companion service to Jellyfin, allowing users to request new content directly from the web interface. Here’s how I’ve integrated it:
|
||||
|
||||
```yaml
|
||||
jellyseerr:
|
||||
image: fallenbagel/jellyseerr:latest
|
||||
container_name: jellyseerr
|
||||
environment:
|
||||
- LOG_LEVEL=debug
|
||||
- TZ=Europe/Prague
|
||||
ports:
|
||||
- 5280:5055
|
||||
volumes:
|
||||
- ./jellyseerr-config:/app/config
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
I’ve exposed Jellyseerr on port `5280` and linked it to the default timezone (`Europe/Prague` in my case). It’s important to store the Jellyseerr configuration separately in the `./jellyseerr-config` directory to persist user settings and requests.
|
||||
|
||||
## Jellystat for Tracking Server Stats
|
||||
|
||||
Jellystat adds another layer of utility, giving you the ability to track detailed statistics about your Jellyfin server usage, including playback metrics, user activity, and media insights.
|
||||
|
||||
It relies on a PostgreSQL database (`jellystat-db` service) to store all the data. I’ve configured it like this:
|
||||
|
||||
```yaml
|
||||
jellystat-db:
|
||||
image: postgres:15.2
|
||||
container_name: jellystat-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: <password>
|
||||
volumes:
|
||||
- ./jellystat-db:/var/lib/postgresql/data
|
||||
|
||||
```
|
||||
|
||||
Jellystat then communicates with the PostgreSQL instance:
|
||||
|
||||
```yaml
|
||||
jellystat:
|
||||
image: cyfershepard/jellystat:latest
|
||||
container_name: jellystat
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: <password>
|
||||
POSTGRES_IP: jellystat-db
|
||||
POSTGRES_PORT: 5432
|
||||
JWT_SECRET: <jwt_secret>
|
||||
TZ: Europe/Prague
|
||||
volumes:
|
||||
- ./jellystat-data:/app/backend/backup-data
|
||||
ports:
|
||||
- "5110:3000"
|
||||
depends_on:
|
||||
- jellystat-db
|
||||
```
|
||||
|
||||
In this configuration, Jellystat is exposed on port `5110`, and I’ve configured the necessary environment variables to link it to the `jellystat-db` container. The JWT secret is used to authenticate requests between Jellystat and Jellyfin.
|
||||
|
||||
# Optimizing and Securing the Setup
|
||||
## 1. Automatic Restart
|
||||
Each service is configured with `restart: unless-stopped`, ensuring that the containers are restarted automatically if they crash or the host reboots. This adds reliability to the deployment, minimizing downtime.
|
||||
|
||||
## 2. Using RAM for Transcoding
|
||||
Offloading transcoding operations to RAM using `/dev/shm` significantly boosts the performance of Jellyfin, especially when dealing with multiple simultaneous streams or high-bitrate media.
|
||||
|
||||
## 3. Resource Isolation and Security
|
||||
By specifying the `user: 1000:1000` directive in the Jellyfin container, I’ve ensured that the Jellyfin service runs as a non-root user on the host. This enhances security by limiting the container's privileges. Also, mounting `/media` in read-only (`ro`) mode ensures that the Jellyfin service cannot modify the media files directly, reducing the risk of accidental data corruption.
|
||||
|
||||
# Conclusion
|
||||
This Docker Compose setup offers a robust Jellyfin media server that takes full advantage of hardware transcoding, secure media access, and useful companion services like Jellyseerr for content requests and Jellystat for usage tracking. Docker makes it easy to maintain, update, and scale this setup with minimal hassle, ensuring you have full control over your media experience.
|
||||
|
||||
With this configuration, you can efficiently manage and monitor your media server, adding a layer of automation and insight to your Jellyfin instance.
|
||||
|
9
content/posts/nextcloud.md
Normal file
9
content/posts/nextcloud.md
Normal file
@ -0,0 +1,9 @@
|
||||
+++
|
||||
title = "Nextcloud"
|
||||
date = 2024-10-03
|
||||
description = "My Nextcloud docker based instance"
|
||||
+++
|
||||
|
||||
This is an post about my docker based Nextcloud instance
|
||||
|
||||
|
91
content/posts/pass-android-yubikey.md
Normal file
91
content/posts/pass-android-yubikey.md
Normal file
@ -0,0 +1,91 @@
|
||||
+++
|
||||
title = "Unix pass and Android Password Store with YubiKey"
|
||||
date = 2025-01-01
|
||||
description = "Setting Up Unix pass with YubiKey and Android Password Store"
|
||||
+++
|
||||
|
||||
Using a secure and versatile password manager is a must for managing your digital life, and [pass](https://www.passwordstore.org/), the Unix password manager, is a fantastic choice. If you store your GPG key on a YubiKey and use the [Android Password Store](https://github.com/android-password-store/Android-Password-Store) app with [OpenKeychain](https://github.com/open-keychain/open-keychain), this guide will help you set everything up while addressing a common compatibility issue with `throw-keyids` in `gpg.conf`.
|
||||
|
||||
## What You’ll Need
|
||||
|
||||
- A YubiKey configured with your GPG key (see the excellent [YubiKey-Guide by drduh](https://github.com/drduh/YubiKey-Guide)).
|
||||
- The [pass](https://www.passwordstore.org/) command-line utility.
|
||||
- [OpenKeychain](https://github.com/open-keychain/open-keychain) installed on your Android device.
|
||||
- The [Android Password Store](https://github.com/android-password-store/Android-Password-Store) app.
|
||||
|
||||
## Configuring `pass` with a YubiKey-Stored GPG Key
|
||||
|
||||
1. **Set up your YubiKey and GPG key**
|
||||
- Follow the steps in [drduh’s YubiKey guide](https://github.com/drduh/YubiKey-Guide) to create and configure your GPG key on your YubiKey.
|
||||
|
||||
2. **Install `pass`**
|
||||
- Install the `pass` utility on your Linux system. Most distributions have it in their package repositories:
|
||||
|
||||
```bash
|
||||
apt install pass # For Debian/Ubuntu-based distros
|
||||
xbps-install -S pass # For Void Linux
|
||||
```
|
||||
|
||||
3. **Initialize `pass` with your GPG key**
|
||||
- Run the following command to initialize the `.password-store` directory:
|
||||
|
||||
```bash
|
||||
pass init <KEYID>
|
||||
```
|
||||
- Replace `<KEYID>` with your GPG key ID stored on the YubiKey.
|
||||
|
||||
4. **Sync passwords to your Android device**
|
||||
- Clone your `.password-store` repository to your Android device and set up the `Password Store` app with `OpenKeychain`.
|
||||
|
||||
## The `throw-keyids` Issue
|
||||
|
||||
While working through this issue, I found a helpful discussion in [GitHub issue #173](https://github.com/android-password-store/Android-Password-Store/issues/173) for the Android Password Store repository. This thread provided insights that clarified the root cause of the problem and its resolution.
|
||||
|
||||
During setup, you might encounter an error in the Android Password Store app. OpenKeychain could report that the `.gpg` files are encrypted for a different key, even if they are not. This issue arises due to the `throw-keyids` option in `~/.gnupg/gpg.conf`.
|
||||
|
||||
### What Does `throw-keyids` Do?
|
||||
|
||||
The `throw-keyids` option in `gpg.conf` hides the recipient’s key ID during encryption. While this enhances privacy by preventing others from identifying the intended recipient(s), it can cause issues with OpenKeychain. OpenKeychain relies on visible key IDs to identify the correct decryption key, and without them, it assumes the files were encrypted for an unknown key.
|
||||
|
||||
### Fixing the Issue
|
||||
|
||||
To resolve this, you need to disable `throw-keyids` and re-encrypt your password store.
|
||||
|
||||
1. **Comment Out `throw-keyids` in `gpg.conf`**
|
||||
- Open `~/.gnupg/gpg.conf` in your favorite text editor and comment out the line:
|
||||
|
||||
```
|
||||
# throw-keyids
|
||||
```
|
||||
|
||||
2. **Re-encrypt Your Password Store**
|
||||
- Run the following command to re-encrypt all passwords with the new GPG options:
|
||||
|
||||
```bash
|
||||
PASSWORD_STORE_GPG_OPTS="--no-throw-keyids" pass init <KEYID>
|
||||
```
|
||||
|
||||
- Replace `<KEYID>` with your GPG key ID.
|
||||
|
||||
3. **Optional: Update Your Environment**
|
||||
- To ensure `--no-throw-keyids` is always used, add the following line to your `~/.bashrc`:
|
||||
|
||||
```bash
|
||||
export PASSWORD_STORE_GPG_OPTS='--no-throw-keyids' # Fix for OpenKeychain
|
||||
```
|
||||
|
||||
- While this step is not strictly necessary (since the change in `gpg.conf` resolves the issue), it can serve as a safeguard.
|
||||
|
||||
4. **Sync the Updated Password Store**
|
||||
- Push the updated `.password-store` to your remote repository and pull it on your Android device.
|
||||
|
||||
## Conclusion
|
||||
|
||||
With these steps, you can seamlessly use `pass` with a YubiKey and the Android Password Store app. The issue with `throw-keyids` is a minor hurdle that can be resolved by adjusting your `gpg.conf` and re-encrypting your password store. Disabling `throw-keyids` makes your key IDs visible, which slightly reduces privacy but is necessary for compatibility with OpenKeychain.
|
||||
|
||||
For further details on `pass`, GPG, or YubiKey, refer to their respective documentation. A secure password manager setup like this ensures your sensitive information stays safe across devices.
|
||||
|
||||
# References
|
||||
- [Standard unix password manager](https://www.passwordstore.org/)
|
||||
- [drduh's YubiKey Guide](https://github.com/drduh/YubiKey-Guide)
|
||||
- [Android Password Store issue](https://github.com/android-password-store/Android-Password-Store/issues/173#issuecomment-453686599)
|
7
content/projects/_index.md
Normal file
7
content/projects/_index.md
Normal file
@ -0,0 +1,7 @@
|
||||
+++
|
||||
title = "Projects"
|
||||
template = "project_list.html"
|
||||
page_template = "project.html"
|
||||
sort_by = "date"
|
||||
+++
|
||||
|
20
content/projects/auto-awning-close.md
Normal file
20
content/projects/auto-awning-close.md
Normal file
@ -0,0 +1,20 @@
|
||||
+++
|
||||
title = "Auto Awning Close"
|
||||
date = 2021-04-12
|
||||
description = "Automated awning control system using Arduino Nano and relays. Includes weather and time-based conditions for seamless operation."
|
||||
|
||||
[extra]
|
||||
state = "done"
|
||||
+++
|
||||
|
||||
The **Auto-Awning-Close** is a smart automation project that integrates seamlessly with an existing awning system. Leveraging an Arduino Nano and relays, it allows for effortless, automated control of your awning based on weather conditions, time schedules, or manual inputs via a dedicated remote.
|
||||
|
||||
This project showcases a sleek and functional approach to home automation, featuring PCB and breadboard schematics for a professional and reliable build. It prioritizes integration, ensuring the original remote controller remains operational while introducing new capabilities.
|
||||
|
||||
## **Key Features**
|
||||
- **Weather-Based Automation:** Protect your awning from harsh elements like rain or wind with automatic closure when necessary.
|
||||
- **Time-Based Automation:** Program your awning to open or close at specific times to suit your routine or maximize daylight exposure.
|
||||
- **Seamless Remote Control:** Retain manual control with a secondary remote for added flexibility alongside automation.
|
||||
|
||||
If you're interested in robust, user-friendly home automation solutions, the Auto-Awning-Close project demonstrates how hardware, creativity, and practical design come together.
|
||||
|
18
content/projects/debrepo.md
Normal file
18
content/projects/debrepo.md
Normal file
@ -0,0 +1,18 @@
|
||||
+++
|
||||
title = "Debrepo"
|
||||
date = 2023-05-01
|
||||
description = "A Debian repository management tool"
|
||||
|
||||
[extra]
|
||||
state = "stable"
|
||||
+++
|
||||
|
||||
# Debrepo
|
||||
A Debian repository management tool.
|
||||
|
||||
## About
|
||||
`Debrepo` is a software tool designed for creating and managing Debian repositories for `*.deb` packages, providing a lightweight and user-friendly alternative to more complex tools like `reprepo` or `aptly`. While these alternatives may offer more advanced features, Debrepo focuses on providing essential functionality and ease of use for repository management, allowing users to easily add, remove, and update packages within their repositories. With Debrepo, users can efficiently manage their Debian repositories without the unnecessary complexity of more advanced tools.
|
||||
|
||||
## Links
|
||||
- [Git](https://git.filiprojek.cz/fr/debrepo)
|
||||
|
16
content/projects/dotfiles.md
Normal file
16
content/projects/dotfiles.md
Normal file
@ -0,0 +1,16 @@
|
||||
+++
|
||||
title = "Dotfiles"
|
||||
date = 2019-01-01
|
||||
description = "Collection of my configuration dotfiles"
|
||||
|
||||
[extra]
|
||||
state = "active"
|
||||
+++
|
||||
|
||||
# Dotfiles
|
||||
|
||||
## About
|
||||
- Collection of my configuration dotfiles.
|
||||
## Links
|
||||
- [Git](https://git.filiprojek.cz/fr/dotfiles)
|
||||
|
34
content/projects/fofrweb.md
Normal file
34
content/projects/fofrweb.md
Normal file
@ -0,0 +1,34 @@
|
||||
+++
|
||||
title = "Fofrweb"
|
||||
date = 2019-11-27
|
||||
description = "Custom websites and student projects"
|
||||
|
||||
[extra]
|
||||
state = "active"
|
||||
+++
|
||||
|
||||
# About
|
||||
- Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.
|
||||
|
||||
# Projects
|
||||
|
||||
## Websites
|
||||
### date
|
||||
### links
|
||||
|
||||
## FofrMess
|
||||
### date
|
||||
|
||||
## FofrTasks
|
||||
### date
|
||||
|
||||
## Fofrbazar
|
||||
- Internetová bazarová platforma
|
||||
### 04/2021
|
||||
- Technologie
|
||||
- PHP
|
||||
- Laravel
|
||||
- MySQL
|
||||
- Linux
|
||||
- Apache
|
||||
|
15
content/projects/nork.md
Normal file
15
content/projects/nork.md
Normal file
@ -0,0 +1,15 @@
|
||||
+++
|
||||
title = "Nork"
|
||||
date = 2021-08-13
|
||||
description = "Simple node.js tool that extends express projects"
|
||||
|
||||
[extra]
|
||||
state = "done"
|
||||
+++
|
||||
|
||||
# Nork
|
||||
## About
|
||||
- Simple node.js tool that extends express projects.
|
||||
|
||||
## Links
|
||||
- [Git](https://github.com/filiprojek/nork)
|
17
content/projects/pkmples.md
Normal file
17
content/projects/pkmples.md
Normal file
@ -0,0 +1,17 @@
|
||||
+++
|
||||
title = "pkmples.cz website"
|
||||
date = 2023-12-18
|
||||
description = "Website for PKM Ples written in Zola"
|
||||
|
||||
[extra]
|
||||
state = "done"
|
||||
+++
|
||||
|
||||
Website for PKM Ples written in [Zola](https://getzola.org).
|
||||
|
||||
I am using [Gitea Actions](https://docs.gitea.com/usage/actions/overview) for CI/CD.
|
||||
|
||||
It is available at [pkmples.cz](https://pkmples.cz).
|
||||
|
||||
Source is available on my [Gitea](https://git.filiprojek.cz/fofrweb/com_pkmples.cz).
|
||||
|
21
content/projects/selfhosting.md
Normal file
21
content/projects/selfhosting.md
Normal file
@ -0,0 +1,21 @@
|
||||
+++
|
||||
title = "Self Hosting"
|
||||
date = 2024-10-14
|
||||
description = "My selfhosting services"
|
||||
|
||||
[extra]
|
||||
state = "pending"
|
||||
+++
|
||||
|
||||
# Next Cloud
|
||||
Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.
|
||||
|
||||
# Jellyfin
|
||||
Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.
|
||||
|
||||
# Uptime Kuma
|
||||
Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.
|
||||
|
||||
# Gitea
|
||||
Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.
|
||||
|
22
content/projects/walauncher.md
Normal file
22
content/projects/walauncher.md
Normal file
@ -0,0 +1,22 @@
|
||||
+++
|
||||
title = "WALauncher"
|
||||
date = 2024-12-15
|
||||
description = "A lightweight launcher for your web apps, an alternative to Muximux "
|
||||
|
||||
[extra]
|
||||
state = "done"
|
||||
+++
|
||||
|
||||
The **WALauncher** project is a lightweight, web-based application launcher designed as a modern and streamlined alternative to [Muximux](https://github.com/mescon/Muximux). It’s perfect for managing web applications like Servarr apps but is highly customizable to support any web-based services.
|
||||
|
||||
WALauncher provides a sleek, user-friendly interface for consolidating your web services into a single, accessible dashboard, making it an efficient tool for home servers or personal projects.
|
||||
|
||||
#### **Key Features**
|
||||
- **Minimalistic Design:** Intuitive layout for easy access to all your web apps.
|
||||
- **Docker Support:** Deploy effortlessly using the provided Docker setup for consistent and reliable hosting.
|
||||
- **Customizable:** Configure apps, icons, titles, and additional options to fit your preferences.
|
||||
- **Open Source:** Contribute to or modify the project to suit your needs.
|
||||
|
||||
WALauncher combines simplicity and functionality for managing your web services with minimal overhead. It’s a solution you can easily deploy and adapt to your workflow.
|
||||
|
||||
Visit the [GitHub](https://github.com/filiprojek/walauncher) repository for more details, screenshots, and support. Start managing your web apps with ease!
|
16
content/projects/website.md
Normal file
16
content/projects/website.md
Normal file
@ -0,0 +1,16 @@
|
||||
+++
|
||||
title = "Website"
|
||||
date = 2023-08-29
|
||||
description = "My personal website"
|
||||
|
||||
[extra]
|
||||
state = "in development"
|
||||
+++
|
||||
|
||||
# Website
|
||||
## About
|
||||
- This website is built using the Zola static site generator.
|
||||
|
||||
## Links
|
||||
- [Git](https://git.filiprojek.cz/fr/website)
|
||||
|
36
postbuild.sh
Executable file
36
postbuild.sh
Executable file
@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
public_dir="./public"
|
||||
css_extension=".css"
|
||||
timestamp=$(date +%s)
|
||||
|
||||
# Define an array of folders to skip
|
||||
skip_folders=("img" "svg" "fonts")
|
||||
|
||||
# Function to add timestamp to CSS files and update HTML imports
|
||||
update_css_references() {
|
||||
# Find all CSS files in the root of the public directory
|
||||
for css_file in "$public_dir"/*$css_extension; do
|
||||
if [[ -f "$css_file" ]]; then
|
||||
base_name=$(basename "$css_file" "$css_extension")
|
||||
new_name="${base_name}-${timestamp}${css_extension}"
|
||||
|
||||
# Rename the CSS file
|
||||
mv "$css_file" "$public_dir/$new_name"
|
||||
echo "Renamed: $css_file -> $new_name"
|
||||
|
||||
# Update all HTML files in the root and 'archiv' folder
|
||||
for html_file in "$public_dir"/*.html "$public_dir/archiv/"*.html; do
|
||||
if [[ -f "$html_file" ]]; then
|
||||
sed -i "s|$base_name$css_extension|$new_name|g" "$html_file"
|
||||
echo "Updated references in: $html_file"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Add timestamp to CSS files and update HTML references (including archiv/index.html)
|
||||
update_css_references
|
||||
|
||||
|
16
sass/about.scss
Normal file
16
sass/about.scss
Normal file
@ -0,0 +1,16 @@
|
||||
.about {
|
||||
h2 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
h3, h4, h5, h6, p {
|
||||
margin-top: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: left;
|
||||
}
|
||||
margin-bottom: 3rem; // space for footer
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 2rem 20%;
|
||||
margin: 2rem 30%;
|
||||
}
|
||||
|
||||
.content {
|
||||
@ -18,3 +18,25 @@
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
li::before {
|
||||
content: '•';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color:white;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,3 +12,7 @@
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.disable-scroll {
|
||||
overflow: hidden !important;
|
||||
position: fixed;
|
||||
}
|
||||
|
@ -1,23 +1,78 @@
|
||||
//.project-wrapper {
|
||||
// justify-content: start;
|
||||
// align-items: start;
|
||||
//}
|
||||
//
|
||||
//.left-bar {
|
||||
// width: 15rem;
|
||||
// justify-content: start;
|
||||
// align-items: start;
|
||||
// border-right: thin solid var(--c-blue);
|
||||
// margin-right: 2.5rem;
|
||||
// padding: 0 2.5rem;
|
||||
//
|
||||
// h2 {
|
||||
// padding-bottom: 2.5rem;
|
||||
// }
|
||||
//}
|
||||
//.language-yaml {
|
||||
pre {
|
||||
display: flex; /* Enables flexbox */
|
||||
align-items: center; /* Vertically centers the content */
|
||||
justify-content: space-between; /* Allows space for the copy button on the right */
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
min-height: 3rem; /* Maintains a consistent height */
|
||||
padding: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
border-radius: 5px;
|
||||
white-space: pre; /* Keeps code formatting */
|
||||
box-sizing: border-box; /* Includes padding in width calculation */
|
||||
}
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
word-break: break-word; /* Prevents long words from breaking the layout */
|
||||
white-space: pre-wrap; /* Allows wrapping of long strings */
|
||||
}
|
||||
|
||||
.project-wrapper {
|
||||
display: flex;
|
||||
gap: 2.5rem;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
|
||||
h1 {
|
||||
margin-top: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h3, h4, h5, h6, p {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.link-back {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.copy-code-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.copy-code {
|
||||
position: absolute;
|
||||
top: .5rem;
|
||||
right: .5rem;
|
||||
cursor: pointer;
|
||||
background-color: #1b1f26;
|
||||
padding: .3rem ;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.project-wrapper {
|
||||
max-width: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.project-wrapper {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,4 +22,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
background: var(--background);
|
||||
color: var(--color);
|
||||
@ -50,11 +50,18 @@ nav {
|
||||
color: var(--color);
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: lightblue;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a { color: var(--color); }
|
||||
a:visited { color: var(--color); }
|
||||
a:hover { color: var(--a-hover); }
|
||||
|
||||
@media (min-width: 400px) {
|
||||
@ -64,11 +71,6 @@ a:hover { color: var(--a-hover); }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
.content {
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
*/
|
||||
footer {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
@ -77,6 +79,41 @@ footer {
|
||||
align-items: center;
|
||||
align-self: flex-end;
|
||||
margin-top: auto;
|
||||
margin-bottom: 2rem;
|
||||
margin-bottom: max(2rem, env(safe-area-inset-bottom)); /* Account for safe area */
|
||||
|
||||
}
|
||||
|
||||
.hamburger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 0px) and (max-width: 1090px) {
|
||||
header {
|
||||
.hamburger {
|
||||
display: block;
|
||||
font-size: 3rem;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
z-index: 2000;
|
||||
}
|
||||
.logo {
|
||||
z-index: 2000;
|
||||
}
|
||||
.hamburger:hover {
|
||||
color: white;
|
||||
}
|
||||
.links {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
background: var(--c-gray);
|
||||
flex-direction: column;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,3 +12,4 @@
|
||||
--color: var(--c-white);
|
||||
--background: var(--c-gray);
|
||||
}
|
||||
|
||||
|
30
static/js/code-copy.js
Normal file
30
static/js/code-copy.js
Normal file
@ -0,0 +1,30 @@
|
||||
let codes = document.querySelectorAll("pre")
|
||||
|
||||
codes.forEach(code => {
|
||||
const elWrapper = document.createElement("div")
|
||||
elWrapper.classList.add("copy-code-wrapper")
|
||||
|
||||
|
||||
const el = document.createElement("span")
|
||||
el.textContent = "Copy"
|
||||
el.classList.add("copy-code")
|
||||
|
||||
elWrapper.appendChild(el)
|
||||
|
||||
code.parentNode.insertBefore(elWrapper, code)
|
||||
|
||||
let textContent = ""
|
||||
code.childNodes.forEach(child => {
|
||||
textContent += child.textContent;
|
||||
})
|
||||
|
||||
elWrapper.addEventListener("click", (e) => {
|
||||
e.preventDefault()
|
||||
navigator.clipboard.writeText(textContent)
|
||||
el.textContent = "Copied!"
|
||||
setTimeout(() => {
|
||||
el.textContent = "Copy"
|
||||
}, 5000);
|
||||
})
|
||||
})
|
||||
|
26
static/js/mobile-navbar.js
Normal file
26
static/js/mobile-navbar.js
Normal file
@ -0,0 +1,26 @@
|
||||
const burger = document.querySelector(".hamburger")
|
||||
const links = document.querySelector(".links")
|
||||
const body = document.querySelector("body")
|
||||
const main = document.querySelector("main")
|
||||
|
||||
let shown = false
|
||||
|
||||
burger.addEventListener("click", (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!shown) {
|
||||
links.style.display = "flex"
|
||||
body.classList.add("disable-scroll")
|
||||
burger.textContent = "x"
|
||||
main.style.visibility = "hidden"
|
||||
} else {
|
||||
links.style.display = "none"
|
||||
body.classList.remove("disable-scroll")
|
||||
burger.textContent = "☰"
|
||||
main.style.visibility = "visible"
|
||||
}
|
||||
|
||||
shown = !shown
|
||||
})
|
||||
|
||||
|
@ -17,12 +17,15 @@
|
||||
</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Filip Rojek - Personal website">
|
||||
<link rel="icon" type="image/x-icon" href="/img/fr.ico">
|
||||
<link rel="stylesheet" href="/general.css">
|
||||
<link rel="stylesheet" href="/vars.css">
|
||||
<link rel="stylesheet" href="/content.css">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<link rel="stylesheet" href="/home.css">
|
||||
<script defer src="https://analytics.fofrweb.com/script.js" data-website-id="2b326fdd-6c87-4627-b1f1-d0afb40aeef6"></script>
|
||||
<script defer src="/js/mobile-navbar.js"></script>
|
||||
{% block styles %}
|
||||
{% endblock styles %}
|
||||
</head>
|
||||
@ -32,7 +35,7 @@
|
||||
<a href="/" class="logo">
|
||||
<img src="/img/fr_logo.webp" alt="logo">
|
||||
</a>
|
||||
<!--<div class="links">
|
||||
<div class="links">
|
||||
{% for item in config.extra.nav_items %}
|
||||
<a href="{{ item.path }}"
|
||||
{% if item.path == current_path and item.path != "/" %}
|
||||
@ -40,7 +43,8 @@
|
||||
{% endif %}
|
||||
>{{ item.name }}</a>
|
||||
{% endfor %}
|
||||
</div>-->
|
||||
</div>
|
||||
<div class="hamburger">☰</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@ -56,12 +60,19 @@
|
||||
{% endblock content %}
|
||||
</main>
|
||||
<footer>
|
||||
<p>Build time: {{ now() | date(format="%Y-%m-%d %H:%M") }},
|
||||
{% if config.extra.git %}
|
||||
<a href="{{ config.extra.git }}" target="_blank">Source</a>
|
||||
{% endif %}</p>
|
||||
<p>© filiprojek.cz 2022 - {{ now() | date(format="%Y")}}</p>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
console.log("I heard that a cool frontend developer works for https://fofrweb.com")
|
||||
</script>
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock scripts %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
33
templates/post.html
Normal file
33
templates/post.html
Normal file
@ -0,0 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="/project.css">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/js/code-copy.js" defer></script>
|
||||
{% endblock scripts %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<section class="project-wrapper flex-col">
|
||||
<h2>{{ page.title }}</h2>
|
||||
<!--
|
||||
<section class="left-bar flex-col">
|
||||
<h2>Projects</h2>
|
||||
{% set section = get_section(path=page.ancestors | last) %}
|
||||
{% for project in section.pages %}
|
||||
<a href="{{ project.permalink }}">{{project.title}}</a>
|
||||
{% endfor %}
|
||||
</section>
|
||||
-->
|
||||
|
||||
<!-- <h2>{{ page.title }}</h2> -->
|
||||
<div>
|
||||
<hr>
|
||||
{{ page.content | safe }}
|
||||
</div>
|
||||
<a href="/posts" class="link-back">Back to list of posts</a>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
28
templates/post_list.html
Normal file
28
templates/post_list.html
Normal file
@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="/project_list.css">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block content %}
|
||||
<section class="project-list content">
|
||||
<h1>My Posts</h1>
|
||||
{% for post in section.pages %}
|
||||
<div class="project">
|
||||
<a class="title" href="{{ post.permalink }}">
|
||||
{{ post.title }}
|
||||
</a>
|
||||
<p class="description">
|
||||
{% if post.description %}
|
||||
{{ post.description }}
|
||||
{% else %}
|
||||
…
|
||||
{% endif %}
|
||||
</p>
|
||||
<hr>
|
||||
<p>{{ post.date }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
@ -18,9 +18,10 @@
|
||||
|
||||
<h2>{{ page.title }}</h2>
|
||||
<div>
|
||||
<hr>
|
||||
{{ page.content | safe }}
|
||||
</div>
|
||||
<a href="/projects">Back to list of projects</a>
|
||||
<a href="/projects" class="link-back">Back to list of projects</a>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user